Repository: pwr-Solaar/Solaar

Issue:
Support Multiplatform layout switching (features MULTIPLATFORM and DUALPLATFORM)
**Information**
- Output of `solaar show` for the target device (if applicable):
```
  2: K850 Performance Wireless Keyboard
     Codename     : K850
     Kind         : keyboard
     Wireless PID : 4062
     Protocol     : HID++ 4.5
     Polling rate : 20 ms (50Hz)
     Serial number: 1A943920
        Bootloader: BOT 43.01.B0004
          Firmware: MPK 04.03.B0015
             Other: 
     The power switch is located on the edge of top right corner.
     Supports 31 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
         3: DEVICE NAME            {0005}   
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: DEVICE FRIENDLY NAME   {0007}   
         7: BATTERY STATUS         {1000}   
         8: CHANGE HOST            {1814}   
         9: HOSTS INFO             {1815}   
        10: REPROG CONTROLS V4     {1B04}   
        11: PERSISTENT REMAPPABLE ACTION {1C00}   
        12: K375S FN INVERSION     {40A3}   
        13: ENCRYPTION             {4100}   
        14: LOCK KEY STATE         {4220}   
        15: KEYBOARD DISABLE       {4521}   
        16: MULTIPLATFORM          {4531}   
        17: DFUCONTROL SIGNED      {00C2}   
        18: unknown:1803           {1803}   internal, hidden
        19: CONFIG DEVICE PROPS    {1806}   internal, hidden
        20: OOBSTATE               {1805}   internal, hidden
        21: unknown:1813           {1813}   internal, hidden
        22: unknown:1830           {1830}   internal, hidden
        23: unknown:1861           {1861}   internal, hidden
        24: unknown:1890           {1890}   internal, hidden
        25: unknown:1891           {1891}   internal, hidden
        26: unknown:18A1           {18A1}   internal, hidden
        27: unknown:1DF3           {1DF3}   internal, hidden
        28: unknown:1E00           {1E00}   hidden
        29: unknown:1EB0           {1EB0}   internal, hidden
        30: unknown:18B0           {18B0}   internal, hidden
     Has 21 reprogrammable keys:
         0: unknown:00D1              , default: unknown:00AE                => unknown:00D1              
             nonstandard, divertable, persistently divertable, pos:0, group:0, gmask:0
         1: unknown:00D2              , default: unknown:00AF                => unknown:00D2              
             nonstandard, divertable, persistently divertable, pos:0, group:0, gmask:0
         2: unknown:00D3              , default: unknown:00B0                => unknown:00D3              
             nonstandard, divertable, persistently divertable, pos:0, group:0, gmask:0
         3: unknown:00D5              , default: unknown:00B2                => unknown:00D5              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:1, group:0, gmask:0
         4: unknown:00D6              , default: unknown:00B3                => unknown:00D6              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:2, group:0, gmask:0
         5: unknown:00BD              , default: unknown:0099                => unknown:00BD              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:3, group:0, gmask:0
         6: unknown:00D4              , default: unknown:00B1                => unknown:00D4              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:4, group:0, gmask:0
         7: Previous                  , default: Previous                    => Previous                  
             is FN, FN sensitive, divertable, persistently divertable, pos:5, group:0, gmask:0
         8: Play/Pause                , default: Play/Pause                  => Play/Pause                
             is FN, FN sensitive, divertable, persistently divertable, pos:6, group:0, gmask:0
         9: Next                      , default: Next                        => Next                      
             is FN, FN sensitive, divertable, persistently divertable, pos:7, group:0, gmask:0
        10: Mute                      , default: Mute                        => Mute                      
             is FN, FN sensitive, divertable, persistently divertable, pos:8, group:0, gmask:0
        11: Volume Down               , default: Volume Down                 => Volume Down               
             is FN, FN sensitive, divertable, persistently divertable, pos:9, group:0, gmask:0
        12: Volume Up                 , default: Volume Up                   => Volume Up                 
             is FN, FN sensitive, divertable, persistently divertable, pos:10, group:0, gmask:0
        13: unknown:00C2              , default: WindowsLock                 => unknown:00C2              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:11, group:0, gmask:0
        14: unknown:00DD              , default: unknown:00BA                => unknown:00DD              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:12, group:0, gmask:0
        15: unknown:00BF              , default: unknown:009B                => unknown:00BF              
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, gmask:0
        16: Calculator                , default: Calculator                  => Calculator                
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, gmask:0
        17: unknown:00C1              , default: unknown:0070                => unknown:00C1              
             is FN, divertable, persistently divertable, pos:0, group:0, gmask:0
        18: unknown:00C0              , default: unknown:006F                => unknown:00C0              
             is FN, divertable, persistently divertable, pos:0, group:0, gmask:0
        19: unknown:00DE              , default: Do Nothing One              => unknown:00DE              
             is FN, pos:0, group:0, gmask:0
        20: unknown:0034              , default: Do Nothing One              => unknown:0034              
             nonstandard, pos:0, group:0, gmask:0
     Battery: 90%, discharging.

```

**Is your feature request related to a problem? Please describe.**

A Linux client will always use the keyboard layout of the last connected platform (Windows/Mac).
Let me elaborate:

Consider the following set-up:

- Thunderbolt Docking Station
- Logitech Unifying receiver connected to docking station
- Docking station is used alternately with a MacBook, a Fedora Linux laptop and a Windows laptop
- K850 Keyboard

The K850 keyboard has multi-platform support, i.e. is has keys that are printed for use with Mac (grey labelling) and Windows (white labelling).
When the last connected device was a Mac then the Linux machine will use the keyboard layout corresponding to the grey labelling, i.e. on the K850 this specifically means the Super key will be on the key labelled `Alt / Cmd` (Windows/Mac respectively).
If the last connected device was a Windows computer then it will use the Windows layout (white labelling), i.e. the Super key is the one labelled `Start / Alt Opt` which is physically located left of the `Alt / Cmd` key.

In essence, as a Linux user you will experience a different behaviour depending on which type of operating system was connected last. Basically every time the switch happens you will have to reassess which key is now `Alt` and which one is `Super`.

**Describe the solution you'd like**
Whenever the Keyboard is connected to a Linux client the Windows Layout should be requested.

**Describe alternatives you've considered**
N/A

**Additional context**
I believe this issue could be much more pronounced on keyboard layouts that label even more keys for dual use (i.e. German). I have both a US and a German K850 but haven't been able to test with the German one.

My wild guess is that this is what the `MULTIPLATFORM {4531}` feature does and thus it'd fall under #582 



Discussion:
Ignore this comment.
I believe this request to be unrelated to the issue I described.
My issue is with a platform specific layout being applied to the keyboard on Windows and Mac and the lack thereof on Linux which can have unexpected side effects depending on which layout was last applied before the user boots into or connects a Linux system.
I believe the person you pointed to this issue is referring to the K850's feature of being able to switch between 3 different devices (Bluetooth or Unifying) and a potential ability of the protocol to set the keyboard to one of those three options from the host side.
Yeah, right.   Sorry for the confusion.
In principle it would not be too hard to implement this feature, but I don't have documentation on how it is supposed to work.
@FFY00 Do you have documentation on this feature (4351 probably)?
Getting documentation on features is a slow process.  I have requests for documentation on features where Solaar is not behaving correctly and I want to get this documentation before asking for Multiplatform.

Resolving patch:
diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py
--- a/lib/logitech_receiver/settings.py
+++ b/lib/logitech_receiver/settings.py
@@ -45,7 +45,7 @@ class Setting(object):
     Needs to be instantiated for each specific device."""
     __slots__ = ('name', 'label', 'description', 'kind', 'device_kind', 'feature', '_rw', '_validator', '_device', '_value')
 
-    def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None):
+    def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None, **kwargs):
         assert name
         self.name = name
         self.label = label or name
@@ -434,7 +434,7 @@ class FeatureRW(object):
     default_read_fnid = 0x00
     default_write_fnid = 0x10
 
-    def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid):
+    def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, **kwargs):
         assert isinstance(feature, _NamedInt)
         self.feature = feature
         self.read_fnid = read_fnid
@@ -453,23 +453,30 @@ class FeatureRWMap(FeatureRW):
     kind = _NamedInt(0x02, 'feature')
     default_read_fnid = 0x00
     default_write_fnid = 0x10
-    default_key_bytes = 1
-
-    def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, key_bytes=default_key_bytes):
+    default_key_bytes_count = 1
+
+    def __init__(
+        self,
+        feature,
+        read_fnid=default_read_fnid,
+        write_fnid=default_write_fnid,
+        key_bytes_count=default_key_bytes_count,
+        **kwargs
+    ):
         assert isinstance(feature, _NamedInt)
         self.feature = feature
         self.read_fnid = read_fnid
         self.write_fnid = write_fnid
-        self.key_bytes = key_bytes
+        self.key_bytes_count = key_bytes_count
 
     def read(self, device, key):
         assert self.feature is not None
-        key_bytes = _int2bytes(key, self.key_bytes)
+        key_bytes = _int2bytes(key, self.key_bytes_count)
         return device.feature_request(self.feature, self.read_fnid, key_bytes)
 
     def write(self, device, key, data_bytes):
         assert self.feature is not None
-        key_bytes = _int2bytes(key, self.key_bytes)
+        key_bytes = _int2bytes(key, self.key_bytes_count)
         return device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes)
 
 
@@ -488,7 +495,7 @@ class BooleanValidator(object):
     # mask specifies all the affected bits in the value
     default_mask = 0xFF
 
-    def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask):
+    def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask, **kwargs):
         if isinstance(true_value, int):
             assert isinstance(false_value, int)
             if mask is None:
@@ -593,7 +600,7 @@ class BitFieldValidator(object):
 
     kind = KIND.multiple_toggle
 
-    def __init__(self, options, byte_count=None):
+    def __init__(self, options, byte_count=None, **kwargs):
         assert (isinstance(options, list))
         self.options = options
         self.byte_count = (max(x.bit_length() for x in options) + 7) // 8
@@ -621,14 +628,12 @@ def prepare_write(self, new_value):
 
 
 class ChoicesValidator(object):
-    __slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
-
     kind = KIND.choice
     """Translates between NamedInts and a byte sequence.
     :param choices: a list of NamedInts
     :param bytes_count: the size of the derived byte sequence. If None, it
     will be calculated from the choices."""
-    def __init__(self, choices, bytes_count=None):
+    def __init__(self, choices, bytes_count=None, read_skip_bytes_count=None, write_prefix_bytes=b'', **_ignore):
         assert choices is not None
         assert isinstance(choices, _NamedInts)
         assert len(choices) > 2
@@ -641,9 +646,13 @@ def __init__(self, choices, bytes_count=None):
             assert self._bytes_count <= bytes_count
             self._bytes_count = bytes_count
         assert self._bytes_count < 8
+        self._read_skip_bytes_count = read_skip_bytes_count if read_skip_bytes_count else 0
+        self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b''
+        assert self._bytes_count + self._read_skip_bytes_count <= 14
+        assert self._bytes_count + len(self._write_prefix_bytes) <= 14
 
     def validate_read(self, reply_bytes):
-        reply_value = _bytes2int(reply_bytes[:self._bytes_count])
+        reply_value = _bytes2int(reply_bytes[self._read_skip_bytes_count:self._read_skip_bytes_count + self._bytes_count])
         valid_value = self.choices[reply_value]
         assert valid_value is not None, '%s: failed to validate read value %02X' % (self.__class__.__name__, reply_value)
         return valid_value
@@ -664,13 +673,22 @@ def prepare_write(self, new_value, current_value=None):
         if choice is None:
             raise ValueError('invalid choice %r' % new_value)
         assert isinstance(choice, _NamedInt)
-        return choice.bytes(self._bytes_count)
+        return self._write_prefix_bytes + choice.bytes(self._bytes_count)
 
 
 class ChoicesMapValidator(ChoicesValidator):
     kind = KIND.map_choice
 
-    def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, extra_default=None):
+    def __init__(
+        self,
+        choices_map,
+        key_bytes_count=None,
+        bytes_count=None,
+        read_skip_bytes_count=0,
+        write_prefix_bytes=b'',
+        extra_default=None,
+        **kwargs
+    ):
         assert choices_map is not None
         assert isinstance(choices_map, dict)
         max_key_bits = 0
@@ -682,24 +700,25 @@ def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, val
             for key_value in choices:
                 assert isinstance(key_value, _NamedInt)
                 max_value_bits = max(max_value_bits, key_value.bit_length())
-        self.choices = choices_map
-        self.needs_current_value = False
-        self.extra_default = extra_default
-
         self._key_bytes_count = (max_key_bits + 7) // 8
         if key_bytes_count:
             assert self._key_bytes_count <= key_bytes_count
             self._key_bytes_count = key_bytes_count
-        self._value_bytes_count = (max_value_bits + 7) // 8
-        if value_bytes_count:
-            assert self._value_bytes_count <= value_bytes_count
-            self._value_bytes_count = value_bytes_count
-        self._skip_bytes_count = skip_bytes_count if skip_bytes_count is not None else 0
-        self._bytes_count = self._key_bytes_count + self._skip_bytes_count + self._value_bytes_count
+        self._bytes_count = (max_value_bits + 7) // 8
+        if bytes_count:
+            assert self._bytes_count <= bytes_count
+            self._bytes_count = bytes_count
+        self.choices = choices_map
+        self.needs_current_value = False
+        self.extra_default = extra_default
+        self._read_skip_bytes_count = read_skip_bytes_count if read_skip_bytes_count else 0
+        self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b''
+        assert self._bytes_count + self._read_skip_bytes_count + self._key_bytes_count <= 14
+        assert self._bytes_count + len(self._write_prefix_bytes) + self._key_bytes_count <= 14
 
     def validate_read(self, reply_bytes, key):
-        start = self._key_bytes_count + self._skip_bytes_count
-        end = start + self._value_bytes_count
+        start = self._key_bytes_count + self._read_skip_bytes_count
+        end = start + self._bytes_count
         reply_value = _bytes2int(reply_bytes[start:end])
         # reprogrammable keys starts out as 0, which is not a choice, so don't use assert here
         if self.extra_default is not None and self.extra_default == reply_value:
@@ -712,7 +731,7 @@ def prepare_write(self, key, new_value):
         choices = self.choices[key]
         if new_value not in choices and new_value != self.extra_default:
             raise ValueError('invalid choice %r' % new_value)
-        return _int2bytes(new_value, self._skip_bytes_count + self._value_bytes_count)
+        return self._write_prefix_bytes + new_value.to_bytes(self._bytes_count, 'big')
 
 
 class RangeValidator(object):
@@ -724,7 +743,7 @@ class RangeValidator(object):
     :param max_value: maximum accepted value (inclusive)
     :param bytes_count: the size of the derived byte sequence. If None, it
     will be calculated from the range."""
-    def __init__(self, min_value, max_value, bytes_count=None):
+    def __init__(self, min_value, max_value, bytes_count=None, **kwargs):
         assert max_value > min_value
         self.min_value = min_value
         self.max_value = max_value
diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py
--- a/lib/logitech_receiver/settings_templates.py
+++ b/lib/logitech_receiver/settings_templates.py
@@ -82,8 +82,8 @@ def register_choices(name, register, choices, kind=_KIND.choice, label=None, des
 def feature_toggle(
     name,
     feature,
-    read_function_id=_FeatureRW.default_read_fnid,
-    write_function_id=_FeatureRW.default_write_fnid,
+    read_fnid=_FeatureRW.default_read_fnid,
+    write_fnid=_FeatureRW.default_write_fnid,
     true_value=_BooleanV.default_true,
     false_value=_BooleanV.default_false,
     mask=_BooleanV.default_mask,
@@ -92,7 +92,7 @@ def feature_toggle(
     device_kind=None
 ):
     validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
-    rw = _FeatureRW(feature, read_function_id, write_function_id)
+    rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
     return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind)
 
 
@@ -100,15 +100,15 @@ def feature_bitfield_toggle(
     name,
     feature,
     options,
-    read_function_id=_FeatureRW.default_read_fnid,
-    write_function_id=_FeatureRW.default_write_fnid,
+    read_fnid=_FeatureRW.default_read_fnid,
+    write_fnid=_FeatureRW.default_write_fnid,
     label=None,
     description=None,
     device_kind=None
 ):
     assert options
     validator = _BitFieldV(options)
-    rw = _FeatureRW(feature, read_function_id, write_function_id)
+    rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
     return _BitFieldSetting(
         name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind
     )
@@ -118,8 +118,8 @@ def feature_bitfield_toggle_dynamic(
     name,
     feature,
     options_callback,
-    read_function_id=_FeatureRW.default_read_fnid,
-    write_function_id=_FeatureRW.default_write_fnid,
+    read_fnid=_FeatureRW.default_read_fnid,
+    write_fnid=_FeatureRW.default_write_fnid,
     label=None,
     description=None,
     device_kind=None
@@ -130,8 +130,8 @@ def instantiate(device):
             name,
             feature,
             options,
-            read_function_id=read_function_id,
-            write_function_id=write_function_id,
+            read_fnid=read_fnid,
+            write_fnid=write_fnid,
             label=label,
             description=description,
             device_kind=device_kind
@@ -142,51 +142,21 @@ def instantiate(device):
     return instantiate
 
 
-def feature_choices(
-    name,
-    feature,
-    choices,
-    read_function_id,
-    write_function_id,
-    bytes_count=None,
-    label=None,
-    description=None,
-    device_kind=None
-):
+def feature_choices(name, feature, choices, **kwargs):
     assert choices
-    validator = _ChoicesV(choices, bytes_count=bytes_count)
-    rw = _FeatureRW(feature, read_function_id, write_function_id)
-    return _Setting(
-        name, rw, validator, feature=feature, kind=_KIND.choice, label=label, description=description, device_kind=device_kind
-    )
+    validator = _ChoicesV(choices, **kwargs)
+    rw = _FeatureRW(feature, **kwargs)
+    return _Setting(name, rw, validator, kind=_KIND.choice, **kwargs)
 
 
-def feature_choices_dynamic(
-    name,
-    feature,
-    choices_callback,
-    read_function_id,
-    write_function_id,
-    bytes_count=None,
-    label=None,
-    description=None,
-    device_kind=None
-):
+def feature_choices_dynamic(name, feature, choices_callback, **kwargs):
     # Proxy that obtains choices dynamically from a device
     def instantiate(device):
         # Obtain choices for this feature
         choices = choices_callback(device)
-        setting = feature_choices(
-            name,
-            feature,
-            choices,
-            read_function_id,
-            write_function_id,
-            bytes_count=bytes_count,
-            label=label,
-            description=description,
-            device_kind=device_kind
-        )
+        if not choices:  # no choices, so don't create a setting
+            return None
+        setting = feature_choices(name, feature, choices, **kwargs)
         return setting(device)
 
     instantiate._rw_kind = _FeatureRW.kind
@@ -195,75 +165,20 @@ def instantiate(device):
 
 # maintain a mapping from keys (NamedInts) to one of a list of choices (NamedInts), default is first one
 # the setting is stored as a JSON-compatible object mapping the key int (as a string) to the choice int
-# extra_default is an extra value that comes from the device that also means the default
-def feature_map_choices(
-    name,
-    feature,
-    choicesmap,
-    read_function_id,
-    write_function_id,
-    key_bytes_count=None,
-    skip_bytes_count=None,
-    value_bytes_count=None,
-    label=None,
-    description=None,
-    device_kind=None,
-    extra_default=None
-):
+def feature_map_choices(name, feature, choicesmap, **kwargs):
     assert choicesmap
-    validator = _ChoicesMapV(
-        choicesmap,
-        key_bytes_count=key_bytes_count,
-        skip_bytes_count=skip_bytes_count,
-        value_bytes_count=value_bytes_count,
-        extra_default=extra_default
-    )
-    rw = _FeatureRWMap(feature, read_function_id, write_function_id, key_bytes=key_bytes_count)
-    return _Settings(
-        name,
-        rw,
-        validator,
-        feature=feature,
-        kind=_KIND.map_choice,
-        label=label,
-        description=description,
-        device_kind=device_kind
-    )
+    validator = _ChoicesMapV(choicesmap, **kwargs)
+    rw = _FeatureRWMap(feature, **kwargs)
+    return _Settings(name, rw, validator, kind=_KIND.map_choice, **kwargs)
 
 
-def feature_map_choices_dynamic(
-    name,
-    feature,
-    choices_callback,
-    read_function_id,
-    write_function_id,
-    key_bytes_count=None,
-    skip_bytes_count=None,
-    value_bytes_count=None,
-    label=None,
-    description=None,
-    device_kind=None,
-    extra_default=None
-):
+def feature_map_choices_dynamic(name, feature, choices_callback, **kwargs):
     # Proxy that obtains choices dynamically from a device
     def instantiate(device):
         choices = choices_callback(device)
         if not choices:  # no choices, so don't create a Setting
             return None
-        setting = feature_map_choices(
-            name,
-            feature,
-            choices,
-            read_function_id,
-            write_function_id,
-            key_bytes_count=key_bytes_count,
-            skip_bytes_count=skip_bytes_count,
-            value_bytes_count=value_bytes_count,
-            label=label,
-            description=description,
-            device_kind=device_kind,
-            extra_default=extra_default
-        )
+        setting = feature_map_choices(name, feature, choices, **kwargs)
         return setting(device)
 
     instantiate._rw_kind = _FeatureRWMap.kind
@@ -275,8 +190,8 @@ def feature_range(
     feature,
     min_value,
     max_value,
-    read_function_id=_FeatureRW.default_read_fnid,
-    write_function_id=_FeatureRW.default_write_fnid,
+    read_fnid=_FeatureRW.default_read_fnid,
+    write_fnid=_FeatureRW.default_write_fnid,
     rw=None,
     bytes_count=None,
     label=None,
@@ -285,7 +200,7 @@ def feature_range(
 ):
     validator = _RangeV(min_value, max_value, bytes_count=bytes_count)
     if rw is None:
-        rw = _FeatureRW(feature, read_function_id, write_function_id)
+        rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
     return _Setting(
         name, rw, validator, feature=feature, kind=_KIND.range, label=label, description=description, device_kind=device_kind
     )
@@ -342,10 +257,22 @@ def feature_range(
     _('Changing important actions (such as for the left mouse button) can result in an unusable system.')
 )
 _DISABLE_KEYS = ('disable-keyboard-keys', _('Disable keys'), _('Disable specific keyboard keys.'))
+_PLATFORM = ('multiplatform', _('Set OS'), _('Change keys to match OS.'))
 
 #
-#
-#
+# Keyword arguments for setting template functions:
+#  label, description - label and tooltip to be shown in GUI
+#  device_kind - the kinds of devices that setting is suitable for (NOT CURRENTLY USED)
+#  read_fnid, write_fnid - default 0x00 and 0x10 function numbers (times 16) to read and write setting
+#  bytes_count - default 1 - number of bytes for the data (ignoring the key)
+# only for boolean settings
+#  true_value, false_value,  mask - integer or byte strings for boolean settings
+# only for map choices
+#  key_bytes_count - default 1 - number of bytes in the key
+#  extra_default - extra value that cannot be set but means same as default value
+# only for choices and map choices
+#  read_skip_bytes_count - default 0 - number of bytes to ignore before the data when reading
+#  write_prefix_bytes - default None - bytes to put before the data writing
 
 
 def _register_hand_detection(
@@ -463,8 +390,8 @@ def _feature_hires_smooth_invert():
     return feature_toggle(
         _HIRES_INV[0],
         _F.HIRES_WHEEL,
-        read_function_id=0x10,
-        write_function_id=0x20,
+        read_fnid=0x10,
+        write_fnid=0x20,
         true_value=0x04,
         mask=0x04,
         label=_HIRES_INV[1],
@@ -477,8 +404,8 @@ def _feature_hires_smooth_resolution():
     return feature_toggle(
         _HIRES_RES[0],
         _F.HIRES_WHEEL,
-        read_function_id=0x10,
-        write_function_id=0x20,
+        read_fnid=0x10,
+        write_fnid=0x20,
         true_value=0x02,
         mask=0x02,
         label=_HIRES_RES[1],
@@ -562,8 +489,8 @@ def _feature_adjustable_dpi():
         _DPI[0],
         _F.ADJUSTABLE_DPI,
         _feature_adjustable_dpi_choices,
-        read_function_id=0x20,
-        write_function_id=0x30,
+        read_fnid=0x20,
+        write_fnid=0x30,
         bytes_count=3,
         label=_DPI[1],
         description=_DPI[2],
@@ -579,8 +506,8 @@ def _feature_pointer_speed():
         _F.POINTER_SPEED,
         0x002e,
         0x01ff,
-        read_function_id=0x0,
-        write_function_id=0x10,
+        read_fnid=0x0,
+        write_fnid=0x10,
         bytes_count=2,
         label=_POINTER_SPEED[1],
         description=_POINTER_SPEED[2],
@@ -624,15 +551,16 @@ def _feature_reprogrammable_keys():
         _REPROGRAMMABLE_KEYS[0],
         _F.REPROG_CONTROLS_V4,
         _feature_reprogrammable_keys_choices,
-        read_function_id=0x20,
-        write_function_id=0x30,
+        read_fnid=0x20,
+        write_fnid=0x30,
         key_bytes_count=2,
-        skip_bytes_count=1,
-        value_bytes_count=2,
+        bytes_count=2,
+        read_skip_bytes_count=1,
+        write_prefix_bytes=b'\x00',
+        extra_default=0,
         label=_REPROGRAMMABLE_KEYS[1],
         description=_REPROGRAMMABLE_KEYS[2],
         device_kind=(_DK.keyboard, ),
-        extra_default=0
     )
 
 
@@ -647,14 +575,83 @@ def _feature_disable_keyboard_keys():
         _DISABLE_KEYS[0],
         _F.KEYBOARD_DISABLE_KEYS,
         _feature_disable_keyboard_keys_key_list,
-        read_function_id=0x10,
-        write_function_id=0x20,
+        read_fnid=0x10,
+        write_fnid=0x20,
         label=_DISABLE_KEYS[1],
         description=_DISABLE_KEYS[2],
         device_kind=(_DK.keyboard, )
     )
 
 
+# muultiplatform OS bits
+OSS = [('Linux', 0x0400), ('MacOS', 0x2000), ('Windows', 0x0100), ('iOS', 0x4000), ('Android', 0x1000), ('WebOS', 0x8000),
+       ('Chrome', 0x0800), ('WinEmb', 0x0200), ('Tizen', 0x0001)]
+
+
+def _feature_multiplatform_choices(device):
+    def _str_os_versions(low, high):
+        def _str_os_version(version):
+            if version == 0:
+                return ''
+            elif version & 0xFF:
+                return str(version >> 8) + '.' + str(version & 0xFF)
+            else:
+                return str(version >> 8)
+
+        return '' if low == 0 and high == 0 else ' ' + _str_os_version(low) + '-' + _str_os_version(high)
+
+    infos = device.feature_request(_F.MULTIPLATFORM)
+    assert infos, 'Oops, multiplatform count cannot be retrieved!'
+    flags, _ignore, num_descriptors = _unpack('!BBB', infos[:3])
+    if not (flags & 0x02):  # can't set platform so don't create setting
+        return []
+    descriptors = []
+    for index in range(0, num_descriptors):
+        descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index)
+        platform, _ignore, os_flags, low, high = _unpack('!BBHHH', descriptor[:8])
+        descriptors.append((platform, os_flags, low, high))
+    choices = _NamedInts()
+    for os_name, os_bit in OSS:
+        for platform, os_flags, low, high in descriptors:
+            os = os_name + _str_os_versions(low, high)
+            if os_bit & os_flags and platform not in choices and os not in choices:
+                choices[platform] = os
+    return choices
+
+
+def _feature_multiplatform():
+    return feature_choices_dynamic(
+        _PLATFORM[0],
+        _F.MULTIPLATFORM,
+        _feature_multiplatform_choices,
+        read_fnid=0x00,
+        read_skip_bytes_count=6,
+        write_fnid=0x30,
+        write_prefix_bytes=b'\xff',
+        label=_PLATFORM[1],
+        description=_PLATFORM[2],
+        device_kind=(_DK.keyboard, )
+    )
+
+
+PLATFORMS = _NamedInts()
+PLATFORMS[0x00] = 'iOS, MacOS'
+PLATFORMS[0x01] = 'Android, Windows'
+
+
+def _feature_dualplatform():
+    return feature_choices(
+        _PLATFORM[0],
+        _F.DUALPLATFORM,
+        PLATFORMS,
+        read_fnid=0x10,
+        write_fnid=0x20,
+        label=_PLATFORM[1],
+        description=_PLATFORM[2],
+        device_kind=(_DK.keyboard, )
+    )
+
+
 #
 #
 #
@@ -681,6 +678,8 @@ def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None):
     _S(_BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2),
     _S(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys),
     _S(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys),
+    _S(_PLATFORM[0], _F.MULTIPLATFORM, _feature_multiplatform),
+    _S(_PLATFORM[0], _F.DUALPLATFORM, _feature_dualplatform, identifier='dualplatform')
 ]
 
 _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE])
diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py
--- a/lib/solaar/ui/config_panel.py
+++ b/lib/solaar/ui/config_panel.py
@@ -90,7 +90,7 @@ def _combo_notify(cbbox, s):
     c = Gtk.ComboBoxText()
     # TODO i18n text entries
     for entry in setting.choices:
-        c.append(str(entry), str(entry))
+        c.append(str(int(entry)), str(entry))
     c.connect('changed', _combo_notify, setting)
     return c
 
@@ -114,7 +114,7 @@ def _map_value_notify_value(cbbox, s):
     def _map_populate_value_box(valueBox, setting, key_choice):
         choices = None
         choices = setting.choices[key_choice]
-        current = setting._value.get(str(key_choice))  # just in case the persisted value is missing some keys
+        current = setting._value.get(str(key_choice)) if setting._value else None
         if choices:
             # TODO i18n text entries
             for choice in choices:


Test patch: