diff --git a/body/board_case.scad b/body/board_case.scad index 618eea8..8e937e5 100644 --- a/body/board_case.scad +++ b/body/board_case.scad @@ -6,11 +6,12 @@ features todo: - [x] magnetic fixation of the keyboard - [x] additional pads fixation for the keyboard - [x] rubber perimeter for better stability +- [ ] make maglet holders 3dprint friendly */ -cut_angle = [-7, 14, 0]; -magnet_radius = 4 / 2 + 0.05; +cut_angle = [-6, 13, 0]; +magnet_radius = 4 / 2 + 0.1; holder_thickness = 0.8; module column3() { @@ -81,8 +82,8 @@ module base_centered() { } module cut_centered() { - rotate([1, -1.5, 0]) - translate([-70, 48, -8]) + rotate([1, -1.0, 0]) + translate([-70, 48, -8.35]) //hull() { import("corne_shape_with_stands.stl"); //} @@ -108,7 +109,7 @@ module shell() { difference() { minkowski() { outline(); - sphere(r=4); + sphere(r=4.2); } minkowski() { @@ -135,6 +136,50 @@ module one_half() { double_magnet_plane(magnet_radius, 2, 0.1); } + + intersection() { + minkowski() { + outline(); + sphere(r=4); + } + rotate(cut_angle) { + print_helpers(); + } + } +} + +module print_helpers() { + translate([30, 35.6, 2.7]) + hull() { + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + + translate([3, 15, 20]) + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + } + + translate([-30, -35.6, 2.7]) + hull() { + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + + translate([0, -10, 10]) + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + } + + translate([-33, 50, 2.7]) + hull() { + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + + translate([0, 5, 10]) + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + } + + translate([33, -50, 2.7]) + hull() { + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + + translate([0, -5, 10]) + cylinder(h=0.5, r=magnet_radius + holder_thickness, $fn=50); + } } one_half(); @@ -159,7 +204,7 @@ module rubber_cuts() { rotate([0, 0, 22.9]) cube([20, 1.8, 1.8], center=true); - translate([44, 32.5, 0]) + translate([44, 33.5, 0]) rotate([0, 0, -22]) cube([20, 1.8, 1.8], center=true); @@ -187,7 +232,7 @@ module double_angled_cuts() { module magnets_plane(radius, height, voffset) { - translate([34, -49.9, voffset]) + translate([33, -49.9, voffset]) cylinder(h=height * 2 + voffset * 2, r=radius, $fn=50, center=true); translate([30, 35.6, voffset]) @@ -237,12 +282,10 @@ module test_magnet() { //corne_centered(); -/* -translate([0, 0, -40]) - rotate([0, 180, 0]) - scale([1, -1, 1]) - corne_centered(); -*/ +//translate([0, 0, -40]) +// rotate([0, 180, 0]) +// scale([1, -1, 1]) +// corne_centered(); /* difference() { diff --git a/body/board_case.stl b/body/board_case.stl index 22bde2b..3755f13 100644 Binary files a/body/board_case.stl and b/body/board_case.stl differ diff --git a/body/body_with_stands.scad b/body/body_with_stands.scad index 889f572..320b5d8 100644 --- a/body/body_with_stands.scad +++ b/body/body_with_stands.scad @@ -1,205 +1,6 @@ -$fn = 50; +include ; -/** - * Corne Chocoflan case body with stands and magnet holes - * - * Based on corne_chocoflan_basic.stl - * - * todo: - * - move two stands 0.5mm up - * - make magnet holes - * - */ +body(); -magnet_radius = 4 / 2 + 0.2; - -magnet1_pos = [8.8, -21.5, 9.4]; -magnet2_pos = [8.8, -56.9, 9.4]; -magnet3_pos = [91.5, -85.0, 8.4]; -magnet4_pos = [101, -14.5, 8.2]; - -module body() { - difference() { - - union() { - import("corne_chocoflan_basic.stl"); - thicknessCubes(); - innerStands(); - - translate(magnet1_pos) - cylinder(h=2.14, r=magnet_radius + 0.5); - - translate(magnet2_pos) - cylinder(h=2.14, r=magnet_radius + 0.5); - - translate(magnet3_pos) - cylinder(h=2.14, r=magnet_radius + 0.5); - - translate(magnet4_pos) - cylinder(h=2.14, r=magnet_radius + 0.5); - } - //import("corne_chocoflan_basic.stl"); - - translate(magnet1_pos) - cylinder(h=4.04, r=magnet_radius); - - translate(magnet2_pos) - cylinder(h=4.04, r=magnet_radius); - - translate(magnet3_pos) - cylinder(h=4.04, r=magnet_radius); - - translate(magnet4_pos) - cylinder(h=4.04, r=magnet_radius); - - rotate([0, 1, 0]) - linearCuts(); - - // standCuts(); - //cube([25,250,100], center=true); - } -} - -//rotate([0,1,0]) -//linearCuts(); - -//translate([8.1,-16.1, 8.5]) -//rotate([0,0.6,0]) -// cube([150,150,0.5]); - -module stand() { - difference() { - union() { - cylinder(h=3, r1=1.5, r2=1.5); - cylinder(h=2, r1=4.5, r2=4); - } - cylinder(h=4, r=1); - } -} - -module innerStands1() { - //cube([18.5,18.5,2]); - translate([18.5, 0, 0]) - stand(); -} - -module innerStands2() { - //cube([18.5,19,2]); - translate([18.5, 19, 0]) - stand(); -} - -module innerStands3() { - //cube([94.75,17,2]); - translate([94.75, 0, 0]) - stand(); -} - -module innerStands4() { - //cube([23.5,64.5,2]); - translate([0, 0, 0]) - stand(); -} - -module innerStands() { - translate([6.1, -32.6, 9.427]) - innerStands1(); - - translate([6.1, -70.5, 9.427]) - innerStands2(); - - translate([6.1, -29.4, 9.427]) - innerStands3(); - - translate([114.4, -77.0, 9.427]) - innerStands4(); -} - -module bottomCube2() { - //cube([3,3,1]); - translate([0, -6, 0]) - cube([38, 6, 1.5]); -} - -module bottomCube1() { - //cube([10,10,1]); - translate([0, 10, 0]) - cube([38, 6, 1.5]); -} - -module thicknessCubes() { - translate([6, -70, 9.2]) - bottomCube1(); - - translate([6, -18, 9.2]) - bottomCube2(); -} - -module standCut() { - cylinder(h=1, r=3, center=true); - difference() { - cylinder(h=1, r=5.5, center=true); - cylinder(h=3, r=3.5, center=true); - } -} - -module linearCut() { - cube([30, 1.6, 1.6]); - translate([0, 1.6 * 2, 0]) - cube([30, 1.6, 1.6]); -} - -module linearCuts() { - - depth = 8.3; - - translate([12, -23.5, depth]) - linearCut(); - - translate([12, -59.5, depth]) - linearCut(); - - translate([95, -80, depth]) - rotate([0, 0, -15]) - linearCut(); - - translate([100, -25.5, depth]) - linearCut(); -} - -//linearCuts(); - -module standCuts() { - zoffset = 9; - translate([24.5, -52, zoffset]) standCut(); - translate([24.5, -32.5, zoffset]) standCut(); - - translate([120, -84, zoffset]) standCut(); - translate([130, -25, zoffset]) standCut(); -} - -module body_solid() { - - union() { - import("corne_shape.stl"); - - rotate([0, 0.7, 0]) - linearCuts(); - - translate([0, 0, -6.8]) { - translate(magnet1_pos) - cylinder(h=6.04, r=magnet_radius); - - translate(magnet2_pos) - cylinder(h=6.04, r=magnet_radius); - - translate(magnet3_pos) - cylinder(h=6.04, r=magnet_radius); - - translate(magnet4_pos) - cylinder(h=6.04, r=magnet_radius); - } - } -} - -body_solid(); +// corne shape with stands +// body_solid(); diff --git a/body/body_with_stands.stl b/body/body_with_stands.stl index 3866a36..fbe27e2 100644 Binary files a/body/body_with_stands.stl and b/body/body_with_stands.stl differ diff --git a/body/corne_body_models.scad b/body/corne_body_models.scad new file mode 100644 index 0000000..caf43f0 --- /dev/null +++ b/body/corne_body_models.scad @@ -0,0 +1,267 @@ +$fn = 50; + +/** + * Corne Chocoflan case body with stands and magnet holes + * + * Based on corne_chocoflan_basic.stl + * + * todo: + * - move two stands 0.5mm up + * - make magnet holes + * + */ + +magnet_radius = 4 / 2 + 0.1; + +magnet1_pos = [8.8, -21.5, 9.4]; +magnet2_pos = [8.8, -56.9, 9.4]; +magnet3_pos = [91.5, -85.0, 9.4]; +magnet4_pos = [101, -14.5, 9.2]; +magnet5_pos = [132, -52.5, 9.2]; + +cuts_rotation = [0, 0.7, 0]; +rubber_width = 5.2; + +module magnets_holders() { + + rotate(cuts_rotation) { + + translate(magnet1_pos) + cylinder(h=2.14, r=magnet_radius + 0.5); + + translate(magnet2_pos) + cylinder(h=2.14, r=magnet_radius + 0.5); + + translate(magnet3_pos) + cylinder(h=2.14, r=magnet_radius + 0.5); + + translate(magnet4_pos) + cylinder(h=2.14, r=magnet_radius + 0.5); + + translate(magnet5_pos) + cylinder(h=2.14, r=magnet_radius + 0.5); + } +} + +module magnets() { + + rotate(cuts_rotation) { + translate(magnet1_pos) + cylinder(h=4.04, r=magnet_radius); + + translate(magnet2_pos) + cylinder(h=4.04, r=magnet_radius); + + translate(magnet3_pos) + cylinder(h=4.04, r=magnet_radius); + + translate(magnet4_pos) + cylinder(h=4.04, r=magnet_radius); + + translate(magnet5_pos) + cylinder(h=4.04, r=magnet_radius); + } +} + +module body() { + difference() { + + union() { + import("corne_chocoflan_basic.stl"); + thicknessCubes(); + innerStands(); + + magnets_holders(); + } + //import("corne_chocoflan_basic.stl"); + + magnets(); + + rotate(cuts_rotation) + linearCutsV2(); + + // standCuts(); + //cube([25,250,100], center=true); + } +} + +//rotate([0,1,0]) +//linearCuts(); + +//translate([8.1,-16.1, 8.5]) +//rotate([0,0.6,0]) +// cube([150,150,0.5]); + +module stand() { + difference() { + union() { + cylinder(h=3, r=1.8); + cylinder(h=2, r1=4.5, r2=4); + } + cylinder(h=4, r=1); + } +} + +module innerStands1() { + //cube([18.5,18.5,2]); + translate([18.5, 0, 0]) + stand(); +} + +module innerStands2() { + //cube([18.5,19,2]); + translate([18.5, 19, 0]) + stand(); +} + +module innerStands3() { + //cube([94.75,17,2]); + translate([94.75, 0, 0]) + stand(); +} + +module innerStands4() { + //cube([23.5,64.5,2]); + translate([0, 0, 0]) + stand(); +} + +module innerStands() { + translate([6.1, -32.6, 9.427]) + innerStands1(); + + translate([6.1, -70.5, 9.427]) + innerStands2(); + + translate([6.0, -29.2, 9.427]) + innerStands3(); + + translate([114.4, -77.0, 9.427]) + innerStands4(); +} + +module bottomCube2() { + //cube([3,3,1]); + translate([0, -6, 0]) + cube([38, 6, 1.5]); +} + +module bottomCube1() { + //cube([10,10,1]); + translate([0, 2, 0]) + cube([38, 52, 1]); +} + +module thicknessCubes() { + rotate(cuts_rotation) + translate([6, -70, 9.3]) + bottomCube1(); + + //translate([6, -18, 9.2]) + //bottomCube2(); +} + +module standCut() { + cylinder(h=1, r=3, center=true); + difference() { + cylinder(h=1, r=5.5, center=true); + cylinder(h=3, r=3.5, center=true); + } +} + +module linearCut() { + cube([30, 1.6, 1.6]); + translate([0, 1.6 * 2, 0]) + cube([30, 1.6, 1.6]); +} + +module linearCuts() { + + depth = 8.3; + + translate([12, -23.5, depth]) + linearCut(); + + translate([12, -59.5, depth]) + linearCut(); + + translate([95, -80, depth]) + rotate([0, 0, -15]) + linearCut(); + + translate([100, -25.5, depth]) + linearCut(); +} + +module linearCutV2() { + cube([50, rubber_width, 1.8]); + translate([0, 7, 0]) + cube([50, rubber_width, 1.8]); + translate([0, 14, 0]) + cube([50, rubber_width, 1.8]); +} + +module linearCutV2_long() { + cube([65, rubber_width, 1.8]); + translate([0, 7, 0]) + cube([66, rubber_width, 1.8]); + translate([0, 14, 0]) + cube([67, rubber_width, 1.8]); +} + +module linearCutsV2() { + + depth = 8.3; + + translate([11.5, -17, depth]) + rotate([0, 0, -90]) + linearCutV2(); + + translate([113, -19, depth]) + rotate([0, 0, -95]) + linearCutV2_long(); +} + +//rotate(cuts_rotation) +//linearCutsV2(); + +module standCuts() { + zoffset = 9; + translate([24.5, -52, zoffset]) standCut(); + translate([24.5, -32.5, zoffset]) standCut(); + + translate([120, -84, zoffset]) standCut(); + translate([130, -25, zoffset]) standCut(); +} + +module body_solid() { + + union() { + //import("corne_shape.stl"); + + translate([70, -50, 13.22]) + rotate(cuts_rotation) + cube([200, 130, 10], center=true); + + rotate(cuts_rotation) + linearCutsV2(); + + rotate(cuts_rotation) + translate([0, 0, -6.8]) { + translate(magnet1_pos) + cylinder(h=6.04, r=magnet_radius); + + translate(magnet2_pos) + cylinder(h=6.04, r=magnet_radius); + + translate(magnet3_pos) + cylinder(h=6.04, r=magnet_radius); + + translate(magnet4_pos) + cylinder(h=6.04, r=magnet_radius); + + translate(magnet5_pos) + cylinder(h=6.04, r=magnet_radius); + } + } +} diff --git a/body/corne_shape_with_stands.scad b/body/corne_shape_with_stands.scad new file mode 100644 index 0000000..089c29e --- /dev/null +++ b/body/corne_shape_with_stands.scad @@ -0,0 +1,5 @@ +include ; + + +// corne shape with stands +body_solid(); diff --git a/body/corne_shape_with_stands.stl b/body/corne_shape_with_stands.stl index d6db6c4..848abf5 100644 Binary files a/body/corne_shape_with_stands.stl and b/body/corne_shape_with_stands.stl differ diff --git a/config/boards/shields/corney/corney.keymap b/config/boards/shields/corney/corney.keymap index 8b4e68f..8a09a25 100644 --- a/config/boards/shields/corney/corney.keymap +++ b/config/boards/shields/corney/corney.keymap @@ -117,10 +117,10 @@ // | SHFT | Z | X | C | V | B | | N | M | , | . | \ | SHFT | // | CTRL| SPACE | LWR | | SPACE| RSE | ALT | bindings = < - &kp TAB &kp Q &kp W &kp E &kp R &kp T &kp Z &kp LBKT &kp I &kp SEMI &kp P &shift_N0 - &cycle_mac_ru &kp SQT &kp MINUS &kp D &kp F &kp G &kp H &kp J &kp K &kp L &altgr_N7 &shift_BSLH - &kp LSHFT &kp Y &kp X &kp C &kp V &kp B &kp N &kp M &altgr_N8 &altgr_N9 &altgr_shift_N7 &kp RSHFT - &kp LCTRL &kp LALT &kp LEFT_COMMAND &kp RCTRL &mo CHARS &kp RALT + &kp TAB &kp Q &kp W &kp E &kp R &kp T &kp Z &kp LBKT &kp I &kp SEMI &kp P &shift_N0 + &cycle_mac_ru &kp SQT &kp MINUS &kp D &kp F &kp G &kp H &kp J &kp K &kp L &altgr_N7 &shift_BSLH + &kp LSHFT &kp Y &kp X &kp C &kp V &kp B &kp N &kp M &altgr_N8 &altgr_N9 &altgr_shift_N7 &kp RSHFT + &kp LCTRL &kp LALT &kp LEFT_COMMAND &kp RCTRL &mo CHARS &kp RALT >; }; diff --git a/config/west.yml b/config/west.yml index b886cbe..9e09dbe 100644 --- a/config/west.yml +++ b/config/west.yml @@ -7,7 +7,7 @@ manifest: projects: - name: zmk remote: zmkfirmware - revision: main + revision: 0.2.1 import: app/west.yml self: path: config diff --git a/docs/gatt-layer-exposition.md b/docs/gatt-layer-exposition.md new file mode 100644 index 0000000..6212267 --- /dev/null +++ b/docs/gatt-layer-exposition.md @@ -0,0 +1,16 @@ +# GATT layer number exposition + +This document defines the custom GATT service and characteristic used to expose the active layer number. + +## UUIDs +- Service UUID: `715d81e1-377d-4f26-a678-a506675d99ec` +- Characteristic UUID (Layer Number): `e87c518a-f323-4dd7-9dd5-0991add1c01b` + +## Data format +- Value: unsigned 8-bit integer +- Endianness: not applicable (single byte) +- Semantics: zero-based active layer number, representing the highest active layer + +## Properties +- Read +- Notify diff --git a/layout_corney.json b/layout_corney.json index a8ed4de..ecbc549 100644 --- a/layout_corney.json +++ b/layout_corney.json @@ -25,19 +25,19 @@ ["Tab", "Tab"], ["q", "KeyQ"], ["w", "KeyW"], ["e", "KeyE"], ["r", "KeyR"], ["t", "KeyT"], ["y", "KeyY"], ["u", "KeyU"], ["i", "KeyI"], ["o", "KeyO"], ["p", "KeyP"], ["-", "Backspace"], ["🌐", "CapsLock"], ["a", "KeyA"], ["s", "KeyS"], ["d", "KeyD"], ["f", "KeyF"], ["g", "KeyG"], ["h", "KeyH"], ["j", "KeyJ"], ["k", "KeyK"], ["l", "KeyL"], [";", "SemiColon"], ["'", "Quote"], ["⇧", "ShiftLeft"], ["z", "KeyZ"], ["x", "KeyX"], ["c", "KeyC"], ["v", "KeyV"], ["b", "KeyB"], ["n", "KeyN"], ["m", "KeyM"], [",", "Comma"], [".", "Dot"], ["/", "Slash"], ["⇧", "ShiftRight"], - ["Ctrl", "ControlLeft"], ["Func", ""], ["SPACE", "Space"], ["SPACE", "Space"], ["Char", ""], ["ALT", "AltGr"] + ["Ctrl", "ControlLeft"], ["SPACE", "Space"], ["Func", ""], ["GUI", "Space"], ["Char", ""], ["ALT", "AltGr"] ], "Default: Linux": [ ["Tab", "Tab"], ["q", "KeyQ"], ["w", "KeyW"], ["e", "KeyE"], ["r", "KeyR"], ["t", "KeyT"], ["y", "KeyZ"], ["u", "KeyU"], ["i", "KeyI"], ["o", "KeyO"], ["p", "KeyP"], ["-", "Slash"], ["🌐", "CapsLock"], ["a", "KeyA"], ["s", "KeyS"], ["d", "KeyD"], ["f", "KeyF"], ["g", "KeyG"], ["h", "KeyH"], ["j", "KeyJ"], ["k", "KeyK"], ["l", "KeyL"], [";", "Comma"], ["'", "BackSlash"], ["⇧", "ShiftLeft"], ["z", "KeyY"], ["x", "KeyX"], ["c", "KeyC"], ["v", "KeyV"], ["b", "KeyB"], ["n", "KeyN"], ["m", "KeyM"], [",", "Comma"], [".", "Dot"], ["/", "Num7"], ["⇧", "ShiftRight"], - ["Ctrl", "ControlLeft"], ["Func", ""], ["SPACE", "Space"], ["SPACE", "Space"], ["Char", ""], ["ALT", "AltGr"] + ["Ctrl", "ControlLeft"], ["SPACE", "Space"], ["Func", ""], ["GUI", "Space"], ["Char", ""], ["ALT", "AltGr"] ], "Default: Mac": [ ["Tab", "Tab"], ["q", "KeyQ"], ["w", "KeyW"], ["e", "KeyE"], ["r", "KeyR"], ["t", "KeyT"], ["y", "KeyZ"], ["u", "KeyU"], ["i", "KeyI"], ["o", "KeyO"], ["p", "KeyP"], ["-", "Slash"], ["🌐", "CapsLock"], ["a", "KeyA"], ["s", "KeyS"], ["d", "KeyD"], ["f", "KeyF"], ["g", "KeyG"], ["h", "KeyH"], ["j", "KeyJ"], ["k", "KeyK"], ["l", "KeyL"], [";", "Comma"], ["'", "BackSlash"], ["⇧", "ShiftLeft"], ["z", "KeyY"], ["x", "KeyX"], ["c", "KeyC"], ["v", "KeyV"], ["b", "KeyB"], ["n", "KeyN"], ["m", "KeyM"], [",", "Comma"], [".", "Dot"], ["/", "Num7"], ["⇧", "ShiftRight"], - ["Ctrl", "ControlLeft"], ["Func", ""], ["SPACE", "Space"], ["SPACE", "Space"], ["Char", ""], ["ALT", "AltGr"] + ["Ctrl", "ControlLeft"], ["SPACE", "Space"], ["Func", ""], ["GUI", "Space"], ["Char", ""], ["ALT", "AltGr"] ], "Linux Shift": [ null, ["Q", "KeyQ"], ["W", "KeyW"], ["E", "KeyE"], ["R", "KeyR"], ["T", "KeyT"], ["Y", "KeyY"], ["U", "KeyU"], ["I", "KeyI"], ["O", "KeyO"], ["P", "KeyP"], ["_", "KeyP"], diff --git a/openspec/changes/add-gatt-layer-exposition/design.md b/openspec/changes/add-gatt-layer-exposition/design.md new file mode 100644 index 0000000..6a2af62 --- /dev/null +++ b/openspec/changes/add-gatt-layer-exposition/design.md @@ -0,0 +1,23 @@ +## Context +ZMK currently exposes BLE HID services, but not a dedicated characteristic for layer state. Companion apps need a lightweight, stable way to read the active layer number. + +## Goals / Non-Goals +- Goals: Provide a GATT characteristic that returns the active layer number and optionally notifies on change. +- Non-Goals: Expose full layer names or keymaps, or provide bi-directional layer control. + +## Decisions +- Decision: Use a dedicated custom GATT service and characteristic to avoid overloading HID semantics. +- Decision: Represent the layer as a zero-based unsigned integer corresponding to the highest active layer. +- Decision: Service UUID `715d81e1-377d-4f26-a678-a506675d99ec` and characteristic UUID `e87c518a-f323-4dd7-9dd5-0991add1c01b`. +- Alternatives considered: Advertising layer in BLE name (too noisy), HID feature reports (tooling complexity). + +## Risks / Trade-offs +- Additional BLE characteristic increases GATT size and may impact power minimally. +- Companion apps must know the UUIDs; coordination/documentation is required. + +## Migration Plan +- Introduce the characteristic behind a config flag enabled by default for this repo. +- Provide documentation and example UUIDs for companion apps. + +## Open Questions +- Do we need to debounce rapid layer changes to avoid notification spam? diff --git a/openspec/changes/add-gatt-layer-exposition/proposal.md b/openspec/changes/add-gatt-layer-exposition/proposal.md new file mode 100644 index 0000000..7fdcf99 --- /dev/null +++ b/openspec/changes/add-gatt-layer-exposition/proposal.md @@ -0,0 +1,13 @@ +# Change: Add GATT layer number exposition + +## Why +Third-party desktop or mobile tools cannot currently read the active layer from the keyboard, which blocks visual layer indicators. Exposing the selected layer over BLE enables companion software to display the current layer state. + +## What Changes +- Add a GATT characteristic that exposes the active layer number. +- Update the layer state pipeline to notify subscribed clients when the layer changes. +- Add configuration defaults and documentation describing the characteristic. + +## Impact +- Affected specs: gatt-layer-exposition +- Affected code: ZMK BLE GATT services, layer state logic, configuration docs diff --git a/openspec/changes/add-gatt-layer-exposition/specs/gatt-layer-exposition/spec.md b/openspec/changes/add-gatt-layer-exposition/specs/gatt-layer-exposition/spec.md new file mode 100644 index 0000000..f21e347 --- /dev/null +++ b/openspec/changes/add-gatt-layer-exposition/specs/gatt-layer-exposition/spec.md @@ -0,0 +1,14 @@ +## ADDED Requirements +### Requirement: Expose active layer number over GATT +The firmware SHALL expose the currently active layer number as a readable GATT characteristic. + +#### Scenario: Read current layer +- **WHEN** a BLE client reads the layer characteristic +- **THEN** the firmware returns the active layer number as an unsigned integer + +### Requirement: Notify on layer changes +The firmware SHALL send GATT notifications for the layer characteristic when the active layer changes and a client has subscribed. + +#### Scenario: Layer change notification +- **WHEN** a BLE client subscribes to notifications and the active layer changes +- **THEN** the firmware sends a notification containing the new active layer number diff --git a/openspec/changes/add-gatt-layer-exposition/tasks.md b/openspec/changes/add-gatt-layer-exposition/tasks.md new file mode 100644 index 0000000..97c6119 --- /dev/null +++ b/openspec/changes/add-gatt-layer-exposition/tasks.md @@ -0,0 +1,6 @@ +## 1. Implementation +- [x] 1.1 Define the GATT service/characteristic UUIDs and data format for layer number exposure +- [ ] 1.2 Add a read characteristic that returns the current active layer number +- [ ] 1.3 Add notify support and emit updates on layer change when clients subscribe +- [ ] 1.4 Add configuration and documentation for enabling/disabling the feature +- [ ] 1.5 Add tests or validation steps for read/notify behavior