-
Notifications
You must be signed in to change notification settings - Fork 145
Post about building a Matter light bulb #626
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: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||||||
| /* | ||||||||||
| * Light control — LEDs 1-3 as a dimmable "light bulb" via PWM. | ||||||||||
| * LED0 is reserved for Matter status indication. | ||||||||||
| * Button 1 (sw0) toggles on/off via callback from Board library. | ||||||||||
| */ | ||||||||||
|
|
||||||||||
| #include "light.h" | ||||||||||
|
|
||||||||||
| #include <zephyr/drivers/pwm.h> | ||||||||||
| #include <zephyr/shell/shell.h> | ||||||||||
| #include <zephyr/logging/log.h> | ||||||||||
|
|
||||||||||
| LOG_MODULE_REGISTER(light, LOG_LEVEL_INF); | ||||||||||
|
|
||||||||||
| static const struct pwm_dt_spec pwm_leds[] = { | ||||||||||
| PWM_DT_SPEC_GET(DT_NODELABEL(pwm_led1)), | ||||||||||
| PWM_DT_SPEC_GET(DT_NODELABEL(pwm_led2)), | ||||||||||
| PWM_DT_SPEC_GET(DT_NODELABEL(pwm_led3)), | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| #define NUM_LEDS ARRAY_SIZE(pwm_leds) | ||||||||||
|
|
||||||||||
|
Comment on lines
+20
to
+22
|
||||||||||
| static bool light_on; | ||||||||||
| static uint8_t light_level = 254; /* default full brightness */ | ||||||||||
|
|
||||||||||
| static void apply_pwm(void) | ||||||||||
| { | ||||||||||
| for (int i = 0; i < NUM_LEDS; i++) { | ||||||||||
| uint32_t pulse; | ||||||||||
|
|
||||||||||
| if (!light_on || light_level == 0) { | ||||||||||
| pulse = 0; | ||||||||||
| } else { | ||||||||||
| /* Map level 1–254 to pulse width (use uint64 to avoid overflow) */ | ||||||||||
| pulse = (uint32_t)(((uint64_t)pwm_leds[i].period * light_level) / 254); | ||||||||||
| } | ||||||||||
| pwm_set_pulse_dt(&pwm_leds[i], pulse); | ||||||||||
| } | ||||||||||
|
Comment on lines
+34
to
+38
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| /* --- Public API --- */ | ||||||||||
|
|
||||||||||
| void light_set(bool on) | ||||||||||
| { | ||||||||||
| light_on = on; | ||||||||||
| apply_pwm(); | ||||||||||
| LOG_INF("Light %s (level %d)", on ? "ON" : "OFF", light_level); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool light_get(void) | ||||||||||
| { | ||||||||||
| return light_on; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| void light_set_level(uint8_t level) | ||||||||||
| { | ||||||||||
| light_level = level; | ||||||||||
| if (level > 0) { | ||||||||||
| light_on = true; | ||||||||||
| } | ||||||||||
|
Comment on lines
+58
to
+60
|
||||||||||
| if (level > 0) { | |
| light_on = true; | |
| } | |
| light_on = (level > 0); |
Copilot
AI
Apr 2, 2026
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.
The shell input is parsed with strtoul() and then cast to uint8_t, so out-of-range values will wrap (e.g., 256 → 0) and 255 can produce an out-of-range PWM pulse. Validate/clamp the value to 0–254 (per the help text) and return an error on invalid input.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #ifndef LIGHT_H_ | ||
| #define LIGHT_H_ | ||
|
|
||
| #include <stdbool.h> | ||
| #include <stdint.h> | ||
|
|
||
| /* Initialize PWM for LEDs. Call AFTER Nrf::GetBoard().Init(). */ | ||
| void light_init(void); | ||
|
|
||
| void light_set(bool on); | ||
| bool light_get(void); | ||
| void light_toggle(void); | ||
|
|
||
| /* Brightness: 0 = off, 1–254 = dim to full */ | ||
| void light_set_level(uint8_t level); | ||
| uint8_t light_get_level(void); | ||
|
|
||
| #endif /* LIGHT_H_ */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| #include <zephyr/dt-bindings/pwm/pwm.h> | ||
|
|
||
| / { | ||
| chosen { | ||
| nordic,pm-ext-flash = &mx25r64; | ||
| }; | ||
|
|
||
| aliases { | ||
| watchdog0 = &wdt31; | ||
| }; | ||
|
|
||
| /* PWM-driven LEDs for dimmable light (LED1-LED3) */ | ||
| pwmleds { | ||
| compatible = "pwm-leds"; | ||
| pwm_led1: pwm_led_1 { | ||
| pwms = <&pwm20 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; | ||
| }; | ||
| pwm_led2: pwm_led_2 { | ||
| pwms = <&pwm20 1 PWM_MSEC(20) PWM_POLARITY_NORMAL>; | ||
| }; | ||
| pwm_led3: pwm_led_3 { | ||
| pwms = <&pwm20 2 PWM_MSEC(20) PWM_POLARITY_NORMAL>; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| /* Override pinctrl to add PWM channels for LED2 and LED3 */ | ||
| &pinctrl { | ||
| pwm20_default: pwm20_default { | ||
| group1 { | ||
| psels = <NRF_PSEL(PWM_OUT0, 1, 25)>, /* LED1: P1.25 */ | ||
| <NRF_PSEL(PWM_OUT1, 1, 27)>, /* LED2: P1.27 */ | ||
| <NRF_PSEL(PWM_OUT2, 1, 28)>; /* LED3: P1.28 */ | ||
| }; | ||
| }; | ||
| pwm20_sleep: pwm20_sleep { | ||
| group1 { | ||
| psels = <NRF_PSEL(PWM_OUT0, 1, 25)>, | ||
| <NRF_PSEL(PWM_OUT1, 1, 27)>, | ||
| <NRF_PSEL(PWM_OUT2, 1, 28)>; | ||
| low-power-enable; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| &rram_controller { | ||
| cpuapp_rram: rram@0 { | ||
| reg = <0x0 DT_SIZE_K(2036)>; | ||
| ranges = <0x0 0x0 DT_SIZE_K(2036)>; | ||
| }; | ||
| }; | ||
|
|
||
| &mx25r64 { | ||
| status = "okay"; | ||
| }; | ||
|
|
||
| &wdt31 { | ||
| status = "okay"; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Compose a Matter ZAP file from fragments. | ||
|
|
||
| Usage: | ||
| python3 zap_compose.py base.json [fragment1.json ...] > output.zap | ||
|
|
||
| The base fragment provides the global config (fileFormat, featureLevel, | ||
| keyValuePairs, package) and the root endpoint. Each additional fragment | ||
| adds an application endpoint. | ||
|
|
||
| Endpoints are assigned IDs sequentially: 0 for the base's endpoint, | ||
| 1 for the first fragment, 2 for the second, etc. | ||
| """ | ||
|
|
||
| import json | ||
| import sys | ||
|
|
||
|
|
||
| def compose(base_path, fragment_paths): | ||
| with open(base_path) as f: | ||
| zap = json.load(f) | ||
|
|
||
| for frag_path in fragment_paths: | ||
| with open(frag_path) as f: | ||
| frag = json.load(f) | ||
|
|
||
| # Append fragment's endpointTypes | ||
| zap["endpointTypes"].extend(frag["endpointTypes"]) | ||
|
|
||
| # Rebuild endpoints array from endpointTypes | ||
| zap["endpoints"] = [] | ||
| for i, ep_type in enumerate(zap["endpointTypes"]): | ||
| zap["endpoints"].append( | ||
| { | ||
| "endpointTypeName": ep_type["name"], | ||
| "endpointTypeIndex": i, | ||
| "profileId": 259, # Matter profile | ||
| "endpointId": i, | ||
| "networkId": 0, | ||
| "parentEndpointIdentifier": None, | ||
| } | ||
| ) | ||
|
|
||
| return zap | ||
|
|
||
|
|
||
| def main(): | ||
| if len(sys.argv) < 2: | ||
| print(f"Usage: {sys.argv[0]} base.json [fragment.json ...]", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| base_path = sys.argv[1] | ||
| fragment_paths = sys.argv[2:] | ||
|
|
||
| zap = compose(base_path, fragment_paths) | ||
| json.dump(zap, sys.stdout, indent=2) | ||
| print() # trailing newline | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Decompose a Matter ZAP file into composable fragments. | ||
|
|
||
| Usage: | ||
| python3 zap_decompose.py input.zap output_dir/ | ||
|
|
||
| Creates: | ||
| output_dir/base.json — root endpoint + global config | ||
| output_dir/<endpoint_name>.json — one file per application endpoint | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
| import re | ||
| import sys | ||
|
|
||
|
|
||
| def slugify(name): | ||
| """Convert endpoint name to a filename-safe slug.""" | ||
| return re.sub(r"[^a-z0-9]+", "_", name.lower()).strip("_") | ||
|
|
||
|
|
||
| def decompose(zap_path, output_dir): | ||
| with open(zap_path) as f: | ||
| zap = json.load(f) | ||
|
|
||
| os.makedirs(output_dir, exist_ok=True) | ||
|
|
||
| endpoint_types = zap["endpointTypes"] | ||
|
|
||
| # Base fragment: global config + first endpointType (root) | ||
| base = { | ||
| "fileFormat": zap["fileFormat"], | ||
| "featureLevel": zap["featureLevel"], | ||
| "creator": zap["creator"], | ||
| "keyValuePairs": zap["keyValuePairs"], | ||
| "package": zap["package"], | ||
| "endpointTypes": [endpoint_types[0]], | ||
| } | ||
|
|
||
| base_path = os.path.join(output_dir, "base.json") | ||
| with open(base_path, "w") as f: | ||
| json.dump(base, f, indent=2) | ||
| f.write("\n") | ||
| print(f" {base_path} ({endpoint_types[0]['name']})") | ||
|
|
||
| # Application fragments: one per remaining endpointType | ||
| for ep_type in endpoint_types[1:]: | ||
| frag = {"endpointTypes": [ep_type]} | ||
| name = slugify(ep_type["name"]) | ||
| frag_path = os.path.join(output_dir, f"{name}.json") | ||
| with open(frag_path, "w") as f: | ||
| json.dump(frag, f, indent=2) | ||
| f.write("\n") | ||
| print(f" {frag_path} ({ep_type['name']})") | ||
|
|
||
|
|
||
| def main(): | ||
| if len(sys.argv) != 3: | ||
| print(f"Usage: {sys.argv[0]} input.zap output_dir/", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| decompose(sys.argv[1], sys.argv[2]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
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.
strtoul() is used below but <stdlib.h> isn’t included, which can trigger a build failure under C99/C11 (implicit declaration). Add the proper standard header.