Mercurial > louis > mq > lightsd
changeset 521:e1ef7138e2b4
got controlgroup working
author | Louis Opter <kalessin@kalessin.fr> |
---|---|
date | Tue, 15 Nov 2016 04:02:08 -0800 |
parents | 8386d8317085 |
children | 737ed6df3f67 |
files | add_monolight.patch |
diffstat | 1 files changed, 245 insertions(+), 172 deletions(-) [+] |
line wrap: on
line diff
--- a/add_monolight.patch Mon Nov 14 18:12:39 2016 -0800 +++ b/add_monolight.patch Tue Nov 15 04:02:08 2016 -0800 @@ -119,7 +119,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/grids.py -@@ -0,0 +1,226 @@ +@@ -0,0 +1,228 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# # This file is part of lightsd. +# @@ -212,10 +212,12 @@ + _self._shift = shift + + def set(_self, offset: Position, level: LedLevel) -> None: -+ offset.x += _self._shift.x -+ offset.y += _self._shift.y ++ offset += _self._shift + return _self._canvas.set(offset, level) + ++ def shift(_self, offset: Position) -> LedCanvas: ++ return cast(LedCanvas, _Proxy(_self, offset)) ++ + def __getattr__(self, name: str) -> Any: + return self._canvas.__getattribute__(name) + # I guess some kind of interface would avoid the cast, but whatever: @@ -445,7 +447,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/types.py -@@ -0,0 +1,62 @@ +@@ -0,0 +1,71 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -474,6 +476,11 @@ + self.x = x + self.y = y + ++ def __copy__(self) -> "Position": ++ return Position(self.x, self.y) ++ ++ __deepcopy__ = __copy__ ++ + def __repr__(self) -> str: + return "{}, {}".format(self.x, self.y) + @@ -493,10 +500,14 @@ + def __repr__(self) -> str: + return "height={}, width={}".format(self.height, self.width) + -+ def __mul__(self, other: "Dimensions") -> "Dimensions": ++ def __sub__(self, other: "Dimensions") -> "Dimensions": + return Dimensions( -+ height=self.height * other.height, -+ width=self.width * other.width, ++ height=self.height - other.height, width=self.width - other.width ++ ) ++ ++ def __add__(self, other: "Dimensions") -> "Dimensions": ++ return Dimensions( ++ height=self.height + other.height, width=self.width + other.width + ) + + @property @@ -647,7 +658,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui/elements/__init__.py -@@ -0,0 +1,27 @@ +@@ -0,0 +1,33 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -666,10 +677,16 @@ +# along with lightsd. If not, see <http://www.gnu.org/licenses/>. + +from .base import UILayer # noqa -+from .buttons import Button # noqa ++from .buttons import ( # noqa ++ Button, ++ PowerButton, ++) ++from .groups import ( # noqa ++ HSBKControlPad, ++ BulbControlPad, ++) +from .sliders import ( # noqa + BrightnessSlider, -+ HSBKPad, + HueSlider, + KelvinSlider, + SaturationSlider, @@ -679,7 +696,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui/elements/base.py -@@ -0,0 +1,255 @@ +@@ -0,0 +1,266 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -698,6 +715,7 @@ +# along with lightsd. If not, see <http://www.gnu.org/licenses/>. + +import asyncio ++import copy +import enum +import logging +import os @@ -740,6 +758,7 @@ + self.actions = actions if actions is not None else {} + for action in self.actions.values(): + action.set_source(self) ++ self.parent = None # type: UIComponent + + if loop is not None: + qsize = self.ACTION_QUEUE_SIZE @@ -748,7 +767,7 @@ + self._action_queue_get = None # type: asyncio.Future + self._current_action = None # type: UIActionEnum + -+ self._nw_corner = offset ++ self._nw_corner = offset - Position(1, 1) + self._se_corner = Position( + x=self.offset.x + self.size.width - 1, + y=self.offset.y + self.size.height - 1, @@ -780,10 +799,10 @@ + """ + + return all(( -+ self._nw_corner.x <= other._se_corner.x, -+ self._se_corner.x >= other._nw_corner.x, -+ self._nw_corner.y <= other._se_corner.y, -+ self._se_corner.y >= other._nw_corner.y, ++ self._nw_corner.x < other._se_corner.x, ++ self._se_corner.x > other._nw_corner.x, ++ self._nw_corner.y < other._se_corner.y, ++ self._se_corner.y > other._nw_corner.y, + )) + + async def _process_actions(self) -> None: @@ -866,12 +885,10 @@ + def fits(self, other: "UIComponent") -> bool: + """Return True if ``self`` has enough space to contain ``other``.""" + -+ return all(( -+ other._nw_corner.x >= 0, -+ other._nw_corner.y >= 0, -+ other._se_corner.x < self.size.width, -+ other._se_corner.y < self.size.height, -+ )) ++ return ( ++ other._se_corner.x < self.size.width and ++ other._se_corner.y < self.size.height ++ ) + + def insert(self, new: "UIComponent") -> None: + if new in self.children: @@ -888,13 +905,20 @@ + "{!r} conflicts with {!r}".format(new, child) + ) + ++ new.parent = self + self.children.add(new) + + def submit_input(self, offset: Position, value: grids.KeyState) -> bool: + if self.collides(_UIPosition(offset)): ++ logger.info("{!r}: accepting input at ({!r}): {}".format( ++ self, offset, value ++ )) + self.handle_input(offset - self.offset, value) + return True + ++ logger.info("{!r}: rejecting input at ({!r}): {}".format( ++ self, offset, value ++ )) + return False + + def handle_input(self, offset: Position, value: grids.KeyState) -> None: @@ -905,7 +929,10 @@ + def draw(self, frame_ts_ms: TimeMonotonic, canvas: grids.LedCanvas) -> bool: + dirty = False + for component in self.children: -+ shifted_canvas = canvas.shift(component.offset + self.offset) ++ vec = copy.copy(self.offset) ++ if not isinstance(component, _UIContainer): ++ vec += component.offset ++ shifted_canvas = canvas.shift(vec) + dirty = component.draw(frame_ts_ms, shifted_canvas) or dirty + return dirty + @@ -922,6 +949,7 @@ + ) -> None: + UIComponent.__init__(self, name, offset, size, loop) + for member in members: ++ logger.info("Inserting {!r} into {!r}".format(member, self)) + self.insert(member) + + @@ -939,7 +967,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui/elements/buttons.py -@@ -0,0 +1,76 @@ +@@ -0,0 +1,111 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -959,9 +987,10 @@ + +import asyncio + -+from typing import Dict -+ -+from ... import grids ++from lightsc import requests ++from typing import Dict, List ++ ++from ... import bulbs, grids +from ...types import Dimensions, Position, TimeMonotonic + +from .. import actions @@ -1016,11 +1045,127 @@ + self._action_queue.put_nowait(action) + except asyncio.QueueFull: + logger.warning("{!r}: action queue full".format(self)) ++ ++ ++class PowerButton(Button): ++ ++ def __init__( ++ self, ++ name: str, ++ offset: Position, ++ loop: asyncio.AbstractEventLoop, ++ targets: List[str], ++ ) -> None: ++ Button.__init__(self, name, offset, loop, actions={ ++ Button.ActionEnum.UP: actions.Lightsd( ++ requests=[requests.PowerToggle], targets=targets ++ ) ++ }) ++ self.targets = targets ++ ++ def draw(self, frame_ts_ms: TimeMonotonic, canvas: grids.LedCanvas) -> bool: ++ if self.busy and frame_ts_ms % 1000 // 100 % 2: ++ level = grids.LedLevel.MEDIUM ++ elif any(bulb.power for bulb in bulbs.iter_targets(self.targets)): ++ level = grids.LedLevel.ON ++ else: ++ level = grids.LedLevel.VERY_LOW_3 ++ ++ if level == self._last_level: ++ return False ++ ++ self._last_level = level ++ for offset in self.size.iter_area(): ++ canvas.set(offset, level) ++ ++ return True +diff --git a/apps/monolight/monolight/ui/elements/groups.py b/apps/monolight/monolight/ui/elements/groups.py +new file mode 100644 +--- /dev/null ++++ b/apps/monolight/monolight/ui/elements/groups.py +@@ -0,0 +1,77 @@ ++# Copyright (c) 2016, Louis Opter <louis@opter.org> ++# ++# This file is part of lightsd. ++# ++# lightsd is free software: you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# lightsd is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with lightsd. If not, see <http://www.gnu.org/licenses/>. ++ ++import asyncio ++import functools ++ ++from typing import List ++ ++from ...types import Dimensions, Position ++ ++from .base import UIGroup ++from .buttons import PowerButton ++from .sliders import BrightnessSlider, HueSlider, KelvinSlider, SaturationSlider ++ ++ ++class HSBKControlPad(UIGroup): ++ ++ def __init__( ++ self, ++ name: str, ++ offset: Position, ++ sliders_size: Dimensions, ++ loop: asyncio.AbstractEventLoop, ++ targets: List[str], ++ ) -> None: ++ sliders = [ ++ functools.partial(HueSlider, name="hue"), ++ functools.partial(SaturationSlider, name="saturation"), ++ functools.partial(BrightnessSlider, name="brightness"), ++ functools.partial(KelvinSlider, name="temperature"), ++ ] ++ sliders = [ ++ Slider( ++ offset=Position(i, 0), ++ size=sliders_size, ++ loop=loop, ++ targets=targets, ++ ) ++ for i, Slider in enumerate(sliders) ++ ] ++ group_size = Dimensions(width=len(sliders), height=sliders_size.height) ++ UIGroup.__init__(self, name, offset, group_size, loop, sliders) ++ ++ ++class BulbControlPad(UIGroup): ++ ++ def __init__( ++ self, ++ name: str, ++ offset: Position, ++ loop: asyncio.AbstractEventLoop, ++ targets: List[str], ++ sliders_size: Dimensions, ++ ) -> None: ++ power_btn = PowerButton("toggle power", Position(0, 0), loop, targets) ++ hsbk_pad = HSBKControlPad( ++ "hsbk pad", Position(0, 1), sliders_size, loop, targets ++ ) ++ ++ group_size = Dimensions(width=0, height=1) + hsbk_pad.size ++ UIGroup.__init__(self, name, offset, group_size, loop, [ ++ power_btn, hsbk_pad, ++ ]) diff --git a/apps/monolight/monolight/ui/elements/sliders.py b/apps/monolight/monolight/ui/elements/sliders.py new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui/elements/sliders.py -@@ -0,0 +1,313 @@ +@@ -0,0 +1,272 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -1045,14 +1190,14 @@ +import statistics +import time + -+from typing import Any, Callable, Dict, Iterable, List, NamedTuple, TypeVar ++from typing import Any, Callable, Iterable, List, NamedTuple, TypeVar + +from ... import bulbs, grids +from ...types import Dimensions, Position, TimeMonotonic + +from .. import actions + -+from .base import UIActionEnum, UIComponent, UIGroup, logger ++from .base import UIActionEnum, UIComponent, logger + + +class SliderTraits: @@ -1066,14 +1211,20 @@ + to the targets it tracks. + """ + ++ Controls = NamedTuple( ++ "Controls", [("coarse", float), ("fine", float)] ++ ) ++ + def __init__( + self, + range: range, ++ controls: Controls, + gather_fn: Callable[[List[str]], List[Any]], + consolidate_fn: Callable[[List[Any]], float], + scatter_fn: Callable[[List[str], Any, int], None], + ) -> None: + self.RANGE = range ++ self.controls = controls + self.gather = gather_fn + self.consolidate = consolidate_fn + self.scatter = scatter_fn @@ -1102,11 +1253,16 @@ + offset: Position, + size: Dimensions, + loop: asyncio.AbstractEventLoop, -+ actions: Dict[ActionEnum, actions.Action], + targets: List[str], + traits: SliderTraits, + ) -> None: -+ UIComponent.__init__(self, name, offset, size, loop, actions) ++ controls = traits.controls ++ UIComponent.__init__(self, name, offset, size, loop, { ++ Slider.ActionEnum.COARSE_INC: actions.Slide(controls.coarse), ++ Slider.ActionEnum.FINE_INC: actions.Slide(controls.fine), ++ Slider.ActionEnum.FINE_DEC: actions.Slide(-controls.fine), ++ Slider.ActionEnum.COARSE_DEC: actions.Slide(-controls.coarse), ++ }) + self.value = float(traits.RANGE.start) + self.targets = targets + self.traits = traits @@ -1254,91 +1410,39 @@ + +HueSlider = functools.partial(Slider, traits=SliderTraits( + range=lightsc.constants.HUE_RANGE, ++ controls=SliderTraits.Controls( ++ coarse=lightsc.constants.HUE_RANGE.stop // 20, fine=1 ++ ), + gather_fn=gather_hue, + consolidate_fn=mean_or_none, + scatter_fn=scatter_hue, +)) +SaturationSlider = functools.partial(Slider, traits=SliderTraits( + range=lightsc.constants.SATURATION_RANGE, ++ controls=SliderTraits.Controls(coarse=0.1, fine=0.01), + gather_fn=gather_saturation, + consolidate_fn=mean_or_none, + scatter_fn=scatter_saturation, +)) +BrightnessSlider = functools.partial(Slider, traits=SliderTraits( + range=lightsc.constants.BRIGHTNESS_RANGE, ++ controls=SliderTraits.Controls(coarse=0.1, fine=0.01), + gather_fn=gather_brightness, + consolidate_fn=mean_or_none, + scatter_fn=scatter_brightness, +)) +KelvinSlider = functools.partial(Slider, traits=SliderTraits( + range=lightsc.constants.KELVIN_RANGE, ++ controls=SliderTraits.Controls(coarse=500, fine=100), + gather_fn=gather_temperature, + consolidate_fn=mean_or_none, + scatter_fn=scatter_temperature, +)) -+ -+ -+class HSBKPad(UIGroup): -+ -+ # I feel like this sucks, so I'm keeping it specific to this class -+ # for now, maybe that should just be part of SliderTraits: -+ SliderSteps = NamedTuple( -+ "SliderSteps", [("coarse", float), ("fine", float)] -+ ) -+ -+ def __init__( -+ self, -+ name: str, -+ offset: Position, -+ sliders_size: Dimensions, -+ loop: asyncio.AbstractEventLoop, -+ hue_steps: SliderSteps, -+ sb_steps: SliderSteps, -+ temp_steps: SliderSteps, -+ targets: List[str], -+ ) -> None: -+ sliders = [ -+ functools.partial(HueSlider, name="hue", actions={ -+ Slider.ActionEnum.COARSE_INC: actions.Slide(hue_steps.coarse), -+ Slider.ActionEnum.FINE_INC: actions.Slide(hue_steps.fine), -+ Slider.ActionEnum.FINE_DEC: actions.Slide(-hue_steps.fine), -+ Slider.ActionEnum.COARSE_DEC: actions.Slide(-hue_steps.coarse), -+ }), -+ functools.partial(SaturationSlider, name="saturation", actions={ -+ Slider.ActionEnum.COARSE_INC: actions.Slide(sb_steps.coarse), -+ Slider.ActionEnum.FINE_INC: actions.Slide(sb_steps.fine), -+ Slider.ActionEnum.FINE_DEC: actions.Slide(-sb_steps.fine), -+ Slider.ActionEnum.COARSE_DEC: actions.Slide(-sb_steps.coarse), -+ }), -+ functools.partial(BrightnessSlider, name="brightness", actions={ -+ Slider.ActionEnum.COARSE_INC: actions.Slide(sb_steps.coarse), -+ Slider.ActionEnum.FINE_INC: actions.Slide(sb_steps.fine), -+ Slider.ActionEnum.FINE_DEC: actions.Slide(-sb_steps.fine), -+ Slider.ActionEnum.COARSE_DEC: actions.Slide(-sb_steps.coarse), -+ }), -+ functools.partial(KelvinSlider, name="temperature", actions={ -+ Slider.ActionEnum.COARSE_INC: actions.Slide(temp_steps.coarse), -+ Slider.ActionEnum.FINE_INC: actions.Slide(temp_steps.fine), -+ Slider.ActionEnum.FINE_DEC: actions.Slide(-temp_steps.fine), -+ Slider.ActionEnum.COARSE_DEC: actions.Slide(-temp_steps.coarse), -+ }), -+ ] -+ sliders = [ -+ Slider( -+ offset=Position(i, 0), -+ size=sliders_size, -+ loop=loop, -+ targets=targets -+ ) -+ for i, Slider in enumerate(sliders) -+ ] -+ group_size = sliders_size * Dimensions(width=len(sliders), height=1) -+ UIGroup.__init__(self, name, offset, group_size, loop, sliders) diff --git a/apps/monolight/monolight/ui/layers.py b/apps/monolight/monolight/ui/layers.py new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui/layers.py -@@ -0,0 +1,153 @@ +@@ -0,0 +1,109 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lightsd. @@ -1361,7 +1465,6 @@ +import logging + +from lightsc import requests -+from lightsc.constants import HUE_RANGE + +from .. import grids +from ..types import Dimensions, Position @@ -1369,8 +1472,9 @@ +from . import actions +from .elements import ( + Button, -+ HSBKPad, ++ PowerButton, + UILayer, ++ groups, +) + +logger = logging.getLogger("monolight.ui.layers") @@ -1389,35 +1493,28 @@ +def root(loop: asyncio.AbstractEventLoop, grid: grids.MonomeGrid) -> UILayer: + foreground_layer = UILayer("root", grid.size, loop) + -+ button = Button("show/hide ui", Position(15, 7), loop, actions={ ++ foreground_layer.insert(Button("toggle ui", Position(15, 7), loop, actions={ + Button.ActionEnum.UP: _ToggleUI(grid), -+ }) -+ foreground_layer.insert(button) -+ button = Button("off *", Position(0, 7), loop, actions={ ++ })) ++ ++ # some shortcuts: ++ foreground_layer.insert(Button("off *", Position(0, 7), loop, actions={ + Button.ActionEnum.UP: actions.Lightsd( + requests=[requests.PowerOff], targets=["*"] + ) -+ }) -+ foreground_layer.insert(button) -+ button = Button("on *", Position(1, 7), loop, actions={ ++ })) ++ foreground_layer.insert(Button("on *", Position(1, 7), loop, actions={ + Button.ActionEnum.UP: actions.Lightsd( + requests=[requests.PowerOn], targets=["*"] + ) -+ }) -+ foreground_layer.insert(button) -+ button = Button("toggle kitchen", Position(2, 7), loop, actions={ -+ Button.ActionEnum.UP: actions.Lightsd( -+ requests=[requests.PowerToggle], targets=["#kitchen"] -+ ) -+ }) -+ foreground_layer.insert(button) -+ button = Button("toggle fugu", Position(3, 7), loop, actions={ -+ Button.ActionEnum.UP: actions.Lightsd( -+ requests=[requests.PowerToggle], targets=["fugu"] -+ ) -+ }) -+ foreground_layer.insert(button) -+ button = Button("orange", Position(4, 7), loop, actions={ ++ })) ++ foreground_layer.insert(PowerButton( ++ "toggle kitchen", Position(2, 7), loop, targets=["#kitchen"] ++ )) ++ foreground_layer.insert(PowerButton( ++ "toggle fugu", Position(3, 7), loop, targets=["fugu"] ++ )) ++ foreground_layer.insert(Button("orange", Position(4, 7), loop, actions={ + Button.ActionEnum.UP: actions.Lightsd(requests=[ + functools.partial( + requests.SetLightFromHSBK, @@ -1433,63 +1530,26 @@ + ), + functools.partial(requests.PowerOn, ["#br"]) + ]), -+ }) -+ foreground_layer.insert(button) -+ -+ coarse_temp_step = 500 -+ fine_temp_step = 100 -+ coarse_hue_step = HUE_RANGE.stop // 20 -+ fine_hue_step = 1 -+ coarse_sb_step = 0.1 -+ fine_sb_step = 0.01 -+ -+ kitchen_control = HSBKPad( -+ "#kitchen hsbk pad", -+ Position(0, 1), -+ Dimensions(width=1, height=6), -+ loop, -+ HSBKPad.SliderSteps(coarse_hue_step, fine_hue_step), -+ HSBKPad.SliderSteps(coarse_sb_step, fine_sb_step), -+ HSBKPad.SliderSteps(coarse_temp_step, fine_temp_step), -+ ["#kitchen"], ++ })) ++ ++ # some control blocks: ++ BulbControlPad = functools.partial( ++ groups.BulbControlPad, ++ loop=loop, ++ sliders_size=Dimensions(width=1, height=6), + ) -+ foreground_layer.insert(kitchen_control) -+ -+ tower_control = HSBKPad( -+ "#tower hsbk pad", -+ Position(4, 1), -+ Dimensions(width=1, height=6), -+ loop, -+ HSBKPad.SliderSteps(coarse_hue_step, fine_hue_step), -+ HSBKPad.SliderSteps(coarse_sb_step, fine_sb_step), -+ HSBKPad.SliderSteps(coarse_temp_step, fine_temp_step), -+ ["#tower"], -+ ) -+ foreground_layer.insert(tower_control) -+ -+ tower_control = HSBKPad( -+ "fugu hsbk pad", -+ Position(8, 1), -+ Dimensions(width=1, height=6), -+ loop, -+ HSBKPad.SliderSteps(coarse_hue_step, fine_hue_step), -+ HSBKPad.SliderSteps(coarse_sb_step, fine_sb_step), -+ HSBKPad.SliderSteps(coarse_temp_step, fine_temp_step), -+ ["fugu"], -+ ) -+ foreground_layer.insert(tower_control) -+ -+ tower_control = HSBKPad( -+ "candle hsbk pad", -+ Position(12, 1), -+ Dimensions(width=1, height=6), -+ loop, -+ HSBKPad.SliderSteps(coarse_hue_step, fine_hue_step), -+ HSBKPad.SliderSteps(coarse_sb_step, fine_sb_step), -+ HSBKPad.SliderSteps(coarse_temp_step, fine_temp_step), -+ ["candle"], -+ ) -+ foreground_layer.insert(tower_control) ++ foreground_layer.insert(BulbControlPad( ++ name="#kitchen control", offset=Position(0, 0), targets=["#kitchen"], ++ )) ++ foreground_layer.insert(BulbControlPad( ++ "#tower control", Position(4, 0), targets=["#tower"], ++ )) ++ foreground_layer.insert(BulbControlPad( ++ "fugu control", Position(8, 0), targets=["fugu"], ++ )) ++ foreground_layer.insert(BulbControlPad( ++ "candle control", Position(12, 0), targets=["candle"], ++ )) + + return foreground_layer diff --git a/apps/monolight/monolight/ui/ui.py b/apps/monolight/monolight/ui/ui.py @@ -1727,7 +1787,7 @@ new file mode 100644 --- /dev/null +++ b/clients/python/lightsc/lightsc/__init__.py -@@ -0,0 +1,42 @@ +@@ -0,0 +1,43 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# All rights reserved. +# @@ -1759,6 +1819,7 @@ + +from . import ( # noqa + client, ++ constants, + exceptions, + requests, + responses, @@ -2527,3 +2588,15 @@ + ], + }, +) +diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c +--- a/lifx/wire_proto.c ++++ b/lifx/wire_proto.c +@@ -95,7 +95,7 @@ + LGTD_LIFX_WIRE_PRINT_TARGET(hdr, target); + lgtd_info( + "%s <-- %s - (Unimplemented, header info: " +- "addressable=%d, tagged=%d, protocol=%d, target=%s", ++ "addressable=%d, tagged=%d, protocol=%d, target=%s)", + pkt_info->name, gw->peeraddr, addressable, tagged, protocol, target + ); + }