Compare commits

..

12 Commits

Author SHA1 Message Date
mkinney
968027a439 Update setup.py 2022-01-15 10:38:40 -08:00
mkinney
cc24b6ebc5 Merge pull request #231 from mkinney/camelCase
handle snake_case or camelCase
2022-01-15 10:32:08 -08:00
Mike Kinney
db09b4718d add two more lines to code coverage 2022-01-15 10:21:24 -08:00
Jm Casler
e0edbc6288 updating proto submodule to latest 2022-01-15 09:34:14 -08:00
Mike Kinney
ae9ae91af5 remove dead code 2022-01-15 00:24:41 -08:00
Mike Kinney
7921db007b add some coverage to getPref() and setPref() 2022-01-15 00:01:44 -08:00
Mike Kinney
e85af2f9e9 Merge branch 'camelCase' of github.com:mkinney/Meshtastic-python into camelCase 2022-01-14 20:16:26 -08:00
Mike Kinney
8ccc64f92e Merge remote-tracking branch 'upstream/master' into camelCase 2022-01-14 20:16:10 -08:00
Mike Kinney
afb21c6dc3 remove code not needed 2022-01-14 20:15:25 -08:00
mkinney
3291bc7097 Merge branch 'meshtastic:master' into camelCase 2022-01-14 20:06:40 -08:00
Mike Kinney
a7d56504be handle snake_case or camelCase 2022-01-14 16:36:53 -08:00
Jm Casler
90e5b473d9 updating proto submodule to latest 2022-01-13 16:55:22 -08:00
10 changed files with 464 additions and 43 deletions

16
exampleConfig.yaml Normal file
View File

@@ -0,0 +1,16 @@
# example config using camelCase keys
owner: Bob TBeam
channelUrl: https://www.meshtastic.org/d/#CgUYAyIBAQ
location:
lat: 35.88888
lon: -93.88888
alt: 304
userPrefs:
region: 1
isAlwaysPowered: 'true'
sendOwnerInterval: 2
screenOnSecs: 31536000
waitBluetoothSecs: 31536000

View File

@@ -1,3 +1,4 @@
# example configuration file with snake_case keys
owner: Bob TBeam
channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ

View File

@@ -20,7 +20,6 @@ from meshtastic import portnums_pb2, channel_pb2, radioconfig_pb2
from meshtastic.globals import Globals
from meshtastic.__init__ import BROADCAST_ADDR
def onReceive(packet, interface):
"""Callback invoked when a packet arrives"""
our_globals = Globals.getInstance()
@@ -56,43 +55,63 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
def getPref(attributes, name):
"""Get a channel or preferences value"""
camel_name = meshtastic.util.snake_to_camel(name)
# Note: protobufs has the keys in snake_case, so snake internally
snake_name = meshtastic.util.camel_to_snake(name)
logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}')
logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}')
objDesc = attributes.DESCRIPTOR
field = objDesc.fields_by_name.get(name)
field = objDesc.fields_by_name.get(snake_name)
if not field:
print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not get it.")
if Globals.getInstance().get_camel_case():
print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not get it.")
else:
print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not get it.")
print(f"Choices in sorted order are:")
names = []
for f in objDesc.fields:
names.append(f'{f.name}')
tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(tmp_name)
for temp_name in sorted(names):
print(f" {temp_name}")
return
# okay - try to read the value
try:
try:
val = getattr(attributes, name)
except TypeError:
# The getter didn't like our arg type guess try again as a string
val = getattr(attributes, name)
# read the value
val = getattr(attributes, snake_name)
# succeeded!
print(f"{name}: {str(val)}")
except Exception as ex:
print(f"Can't get {name} due to {ex}")
if Globals.getInstance().get_camel_case():
print(f"{camel_name}: {str(val)}")
logging.debug(f"{camel_name}: {str(val)}")
else:
print(f"{snake_name}: {str(val)}")
logging.debug(f"{snake_name}: {str(val)}")
def setPref(attributes, name, valStr):
"""Set a channel or preferences value"""
snake_name = meshtastic.util.camel_to_snake(name)
camel_name = meshtastic.util.snake_to_camel(name)
logging.debug(f'snake_name:{snake_name}')
logging.debug(f'camel_name:{camel_name}')
objDesc = attributes.DESCRIPTOR
field = objDesc.fields_by_name.get(name)
field = objDesc.fields_by_name.get(snake_name)
if not field:
print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not set it.")
if Globals.getInstance().get_camel_case():
print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not set it.")
else:
print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not set it.")
print(f"Choices in sorted order are:")
names = []
for f in objDesc.fields:
names.append(f'{f.name}')
tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(tmp_name)
for temp_name in sorted(names):
print(f" {temp_name}")
return
@@ -107,27 +126,27 @@ def setPref(attributes, name, valStr):
if e:
val = e.number
else:
print(f"{name} does not have an enum called {val}, so you can not set it.")
if Globals.getInstance().get_camel_case():
print(f"{camel_name} does not have an enum called {val}, so you can not set it.")
else:
print(f"{snake_name} does not have an enum called {val}, so you can not set it.")
print(f"Choices in sorted order are:")
names = []
for f in enumType.values:
names.append(f'{f.name}')
tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(name)
for temp_name in sorted(names):
print(f" {temp_name}")
return
# okay - try to read the value
try:
try:
setattr(attributes, name, val)
except TypeError:
# The setter didn't like our arg type guess try again as a string
setattr(attributes, name, valStr)
setattr(attributes, snake_name, val)
# succeeded!
print(f"Set {name} to {valStr}")
except Exception as ex:
print(f"Can't set {name} due to {ex}")
if Globals.getInstance().get_camel_case():
print(f"Set {camel_name} to {valStr}")
else:
print(f"Set {snake_name} to {valStr}")
def onConnected(interface):
@@ -311,6 +330,10 @@ def onConnected(interface):
print("Setting channel url to", configuration['channel_url'])
interface.getNode(args.dest).setURL(configuration['channel_url'])
if 'channelUrl' in configuration:
print("Setting channel url to", configuration['channelUrl'])
interface.getNode(args.dest).setURL(configuration['channelUrl'])
if 'location' in configuration:
alt = 0
lat = 0.0
@@ -340,6 +363,13 @@ def onConnected(interface):
print("Writing modified preferences to device")
interface.getNode(args.dest).writeConfig()
if 'userPrefs' in configuration:
prefs = interface.getNode(args.dest).radioConfig.preferences
for pref in configuration['userPrefs']:
setPref(prefs, pref, str(configuration['userPrefs'][pref]))
print("Writing modified preferences to device")
interface.getNode(args.dest).writeConfig()
if args.export_config:
# export the configuration (the opposite of '--configure')
closeNow = True
@@ -548,7 +578,10 @@ def export_config(interface):
if owner:
config += f"owner: {owner}\n\n"
if channel_url:
config += f"channel_url: {channel_url}\n\n"
if Globals.getInstance().get_camel_case():
config += f"channelUrl: {channel_url}\n\n"
else:
config += f"channel_url: {channel_url}\n\n"
if lat or lon or alt:
config += "location:\n"
if lat:
@@ -561,9 +594,16 @@ def export_config(interface):
preferences = f'{interface.localNode.radioConfig.preferences}'
prefs = preferences.splitlines()
if prefs:
config += "user_prefs:\n"
if Globals.getInstance().get_camel_case():
config += "userPrefs:\n"
else:
config += "user_prefs:\n"
for pref in prefs:
config += f" {meshtastic.util.quoteBooleans(pref)}\n"
if Globals.getInstance().get_camel_case():
# Note: This may not work if the value has '_'
config += f" {meshtastic.util.snake_to_camel(meshtastic.util.quoteBooleans(pref))}\n"
else:
config += f" {meshtastic.util.quoteBooleans(pref)}\n"
print(config)
return config
@@ -692,10 +732,12 @@ def initParser():
action="store_true")
parser.add_argument(
"--get", help="Get a preferences field. Use an invalid field such as '0' to get a list of all fields.", nargs=1, action='append')
"--get", help=("Get a preferences field. Use an invalid field such as '0' to get a list of all fields."
" Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')"),
nargs=1, action='append')
parser.add_argument(
"--set", help="Set a preferences field", nargs=2, action='append')
"--set", help="Set a preferences field. Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')", nargs=2, action='append')
parser.add_argument(
"--seturl", help="Set a channel URL", action="store")

View File

@@ -30,6 +30,8 @@ class Globals:
self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
def reset(self):
"""Reset all of our globals. If you add a member, add it to this method, too."""
@@ -38,6 +40,8 @@ class Globals:
self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
# setters
def set_args(self, args):
@@ -60,6 +64,10 @@ class Globals:
"""Set the tunnelInstance"""
self.tunnelInstance = tunnelInstance
def set_camel_case(self):
"""Force using camelCase for things like prefs/set/set"""
self.camel_case = True
# getters
def get_args(self):
"""Get args"""
@@ -80,3 +88,7 @@ class Globals:
def get_tunnelInstance(self):
"""Get tunnelInstance"""
return self.tunnelInstance
def get_camel_case(self):
"""Get whether or not to use camelCase"""
return self.camel_case

View File

@@ -8,8 +8,6 @@ from meshtastic import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2
from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK
class Node:
"""A model of a (local or remote) node in the mesh

View File

@@ -19,6 +19,7 @@ from ..tcp_interface import TCPInterface
from ..node import Node
from ..channel_pb2 import Channel
from ..remote_hardware import onGPIOreceive
from ..radioconfig_pb2 import RadioConfig
@pytest.mark.unit
@@ -366,6 +367,27 @@ def test_main_qr(capsys):
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_onConnected_exception(capsys):
"""Test the exception in onConnected"""
sys.argv = ['', '--qr']
Globals.getInstance().set_args(sys.argv)
def throw_an_exception(junk):
raise Exception("Fake exception.")
iface = MagicMock(autospec=SerialInterface)
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
with patch('pyqrcode.create', side_effect=throw_an_exception):
with pytest.raises(Exception) as pytest_wrapped_e:
main()
out, err = capsys.readouterr()
assert re.search('Aborting due to: Fake exception', out, re.MULTILINE)
assert err == ''
assert pytest_wrapped_e.type == Exception
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_nodes(capsys):
@@ -781,6 +803,28 @@ def test_main_set_valid(capsys):
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_set_valid_camel_case(capsys):
"""Test --set with valid field"""
sys.argv = ['', '--set', 'wifi_ssid', 'foo']
Globals.getInstance().set_args(sys.argv)
Globals.getInstance().set_camel_case()
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r'Connected to radio', out, re.MULTILINE)
assert re.search(r'Set wifiSsid to foo', out, re.MULTILINE)
assert err == ''
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_set_with_invalid(capsys):
@@ -809,7 +853,7 @@ def test_main_set_with_invalid(capsys):
# TODO: write some negative --configure tests
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_configure(capsys):
def test_main_configure_with_snake_case(capsys):
"""Test --configure with valid file"""
sys.argv = ['', '--configure', 'example_config.yaml']
Globals.getInstance().set_args(sys.argv)
@@ -833,6 +877,31 @@ def test_main_configure(capsys):
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_configure_with_camel_case_keys(capsys):
"""Test --configure with valid file"""
sys.argv = ['', '--configure', 'exampleConfig.yaml']
Globals.getInstance().set_args(sys.argv)
mocked_node = MagicMock(autospec=Node)
iface = MagicMock(autospec=SerialInterface)
iface.getNode.return_value = mocked_node
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r'Connected to radio', out, re.MULTILINE)
assert re.search(r'Setting device owner', out, re.MULTILINE)
assert re.search(r'Setting channel url', out, re.MULTILINE)
assert re.search(r'Fixing altitude', out, re.MULTILINE)
assert re.search(r'Fixing latitude', out, re.MULTILINE)
assert re.search(r'Fixing longitude', out, re.MULTILINE)
assert re.search(r'Writing modified preferences', out, re.MULTILINE)
assert err == ''
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_ch_add_valid(capsys):
@@ -1289,6 +1358,33 @@ def test_main_get_with_valid_values(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_get_with_valid_values_camel(capsys, caplog):
"""Test --get with valid values (with string, number, boolean)"""
sys.argv = ['', '--get', 'lsSecs', '--get', 'wifiSsid', '--get', 'fixedPosition']
Globals.getInstance().set_args(sys.argv)
Globals.getInstance().set_camel_case()
with caplog.at_level(logging.DEBUG):
with patch('meshtastic.serial_interface.SerialInterface') as mo:
mo().getNode().radioConfig.preferences.wifi_ssid = 'foo'
mo().getNode().radioConfig.preferences.ls_secs = 300
mo().getNode().radioConfig.preferences.fixed_position = False
main()
mo.assert_called()
out, err = capsys.readouterr()
assert re.search(r'Connected to radio', out, re.MULTILINE)
assert re.search(r'lsSecs: 300', out, re.MULTILINE)
assert re.search(r'wifiSsid: foo', out, re.MULTILINE)
assert re.search(r'fixedPosition: False', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_get_with_invalid(capsys):
@@ -1492,6 +1588,45 @@ position_flags: 35"""
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_export_config_use_camel(capsys):
"""Test export_config() function directly"""
Globals.getInstance().set_camel_case()
iface = MagicMock(autospec=SerialInterface)
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
mo.getLongName.return_value = 'foo'
mo.localNode.getURL.return_value = 'bar'
mo.getMyNodeInfo().get.return_value = { 'latitudeI': 1100000000, 'longitudeI': 1200000000,
'altitude': 100, 'batteryLevel': 34, 'latitude': 110.0,
'longitude': 120.0}
mo.localNode.radioConfig.preferences = """phone_timeout_secs: 900
ls_secs: 300
position_broadcast_smart: true
fixed_position: true
position_flags: 35"""
export_config(mo)
out, err = capsys.readouterr()
# ensure we do not output this line
assert not re.search(r'Connected to radio', out, re.MULTILINE)
assert re.search(r'owner: foo', out, re.MULTILINE)
assert re.search(r'channelUrl: bar', out, re.MULTILINE)
assert re.search(r'location:', out, re.MULTILINE)
assert re.search(r'lat: 110.0', out, re.MULTILINE)
assert re.search(r'lon: 120.0', out, re.MULTILINE)
assert re.search(r'alt: 100', out, re.MULTILINE)
assert re.search(r'userPrefs:', out, re.MULTILINE)
assert re.search(r'phoneTimeoutSecs: 900', out, re.MULTILINE)
assert re.search(r'lsSecs: 300', out, re.MULTILINE)
# TODO: should True be capitalized here?
assert re.search(r"positionBroadcastSmart: 'True'", out, re.MULTILINE)
assert re.search(r"fixedPosition: 'True'", out, re.MULTILINE)
assert re.search(r"positionFlags: 35", out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_export_config_called_from_main(capsys):
@@ -1625,6 +1760,22 @@ def test_main_getPref_valid_field(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_valid_field_camel(capsys):
"""Test getPref() with a valid field"""
Globals.getInstance().set_camel_case()
prefs = MagicMock()
prefs.DESCRIPTOR.fields_by_name.get.return_value = 'ls_secs'
prefs.wifi_ssid = 'foo'
prefs.ls_secs = 300
prefs.fixed_position = False
getPref(prefs, 'ls_secs')
out, err = capsys.readouterr()
assert re.search(r'lsSecs: 300', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_valid_field_string(capsys):
@@ -1641,6 +1792,23 @@ def test_main_getPref_valid_field_string(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_valid_field_string_camel(capsys):
"""Test getPref() with a valid field and value as a string"""
Globals.getInstance().set_camel_case()
prefs = MagicMock()
prefs.DESCRIPTOR.fields_by_name.get.return_value = 'wifi_ssid'
prefs.wifi_ssid = 'foo'
prefs.ls_secs = 300
prefs.fixed_position = False
getPref(prefs, 'wifi_ssid')
out, err = capsys.readouterr()
assert re.search(r'wifiSsid: foo', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_valid_field_bool(capsys):
@@ -1657,6 +1825,23 @@ def test_main_getPref_valid_field_bool(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_valid_field_bool_camel(capsys):
"""Test getPref() with a valid field and value as a bool"""
Globals.getInstance().set_camel_case()
prefs = MagicMock()
prefs.DESCRIPTOR.fields_by_name.get.return_value = 'fixed_position'
prefs.wifi_ssid = 'foo'
prefs.ls_secs = 300
prefs.fixed_position = False
getPref(prefs, 'fixed_position')
out, err = capsys.readouterr()
assert re.search(r'fixedPosition: False', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_getPref_invalid_field(capsys):
@@ -1691,7 +1876,40 @@ def test_main_getPref_invalid_field(capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_int(capsys):
def test_main_getPref_invalid_field_camel(capsys):
"""Test getPref() with an invalid field"""
Globals.getInstance().set_camel_case()
class Field:
"""Simple class for testing."""
def __init__(self, name):
"""constructor"""
self.name = name
prefs = MagicMock()
prefs.DESCRIPTOR.fields_by_name.get.return_value = None
# Note: This is a subset of the real fields
ls_secs_field = Field('ls_secs')
is_router = Field('is_router')
fixed_position = Field('fixed_position')
fields = [ ls_secs_field, is_router, fixed_position ]
prefs.DESCRIPTOR.fields = fields
getPref(prefs, 'foo')
out, err = capsys.readouterr()
assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
# ensure they are sorted
assert re.search(r'fixedPosition\s+isRouter\s+lsSecs', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_int_as_string(capsys):
"""Test setPref() with a valid field"""
class Field:
@@ -1712,6 +1930,74 @@ def test_main_setPref_valid_field_int(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_invalid_enum(capsys, caplog):
"""Test setPref() with a valid field but invalid enum value"""
radioConfig = RadioConfig()
prefs = radioConfig.preferences
with caplog.at_level(logging.DEBUG):
setPref(prefs, 'charge_current', 'foo')
out, err = capsys.readouterr()
assert re.search(r'charge_current does not have an enum called foo', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_invalid_enum_camel(capsys, caplog):
"""Test setPref() with a valid field but invalid enum value"""
Globals.getInstance().set_camel_case()
radioConfig = RadioConfig()
prefs = radioConfig.preferences
with caplog.at_level(logging.DEBUG):
setPref(prefs, 'charge_current', 'foo')
out, err = capsys.readouterr()
assert re.search(r'chargeCurrent does not have an enum called foo', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_valid_enum(capsys, caplog):
"""Test setPref() with a valid field and valid enum value"""
# charge_current
# some valid values: MA100 MA1000 MA1080
radioConfig = RadioConfig()
prefs = radioConfig.preferences
with caplog.at_level(logging.DEBUG):
setPref(prefs, 'charge_current', 'MA100')
out, err = capsys.readouterr()
assert re.search(r'Set charge_current to MA100', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_valid_field_valid_enum_camel(capsys, caplog):
"""Test setPref() with a valid field and valid enum value"""
Globals.getInstance().set_camel_case()
# charge_current
# some valid values: MA100 MA1000 MA1080
radioConfig = RadioConfig()
prefs = radioConfig.preferences
with caplog.at_level(logging.DEBUG):
setPref(prefs, 'charge_current', 'MA100')
out, err = capsys.readouterr()
assert re.search(r'Set chargeCurrent to MA100', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_invalid_field(capsys):
@@ -1744,6 +2030,38 @@ def test_main_setPref_invalid_field(capsys):
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_setPref_invalid_field_camel(capsys):
"""Test setPref() with a invalid field"""
Globals.getInstance().set_camel_case()
class Field:
"""Simple class for testing."""
def __init__(self, name):
"""constructor"""
self.name = name
prefs = MagicMock()
prefs.DESCRIPTOR.fields_by_name.get.return_value = None
# Note: This is a subset of the real fields
ls_secs_field = Field('ls_secs')
is_router = Field('is_router')
fixed_position = Field('fixed_position')
fields = [ ls_secs_field, is_router, fixed_position ]
prefs.DESCRIPTOR.fields = fields
setPref(prefs, 'foo', '300')
out, err = capsys.readouterr()
assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
# ensure they are sorted
assert re.search(r'fixedPosition\s+isRouter\s+lsSecs', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_main_ch_set_psk_no_ch_index(capsys):

View File

@@ -10,7 +10,8 @@ from meshtastic.util import (fixme, stripnl, pskToString, our_exit,
support_info, genPSK256, fromStr, fromPSK,
quoteBooleans, catchAndIgnore,
remove_keys_from_dict, Timeout, hexstr,
ipstr, readnet_u16, findPorts, convert_mac_addr)
ipstr, readnet_u16, findPorts, convert_mac_addr,
snake_to_camel, camel_to_snake)
@pytest.mark.unit
@@ -251,3 +252,22 @@ def test_convert_mac_addr():
assert convert_mac_addr('/c0gFyhb') == 'fd:cd:20:17:28:5b'
assert convert_mac_addr('fd:cd:20:17:28:5b') == 'fd:cd:20:17:28:5b'
assert convert_mac_addr('') == ''
@pytest.mark.unit
def test_snake_to_camel():
"""Test snake_to_camel"""
assert snake_to_camel('') == ''
assert snake_to_camel('foo') == 'foo'
assert snake_to_camel('foo_bar') == 'fooBar'
assert snake_to_camel('fooBar') == 'fooBar'
@pytest.mark.unit
def test_camel_to_snake():
"""Test camel_to_snake"""
assert camel_to_snake('') == ''
assert camel_to_snake('foo') == 'foo'
assert camel_to_snake('Foo') == 'foo'
assert camel_to_snake('fooBar') == 'foo_bar'
assert camel_to_snake('fooBarBaz') == 'foo_bar_baz'

View File

@@ -243,3 +243,17 @@ def convert_mac_addr(val):
val_as_bytes = base64.b64decode(val)
return hexstr(val_as_bytes)
return val
def snake_to_camel(a_string):
"""convert snake_case to camelCase"""
# split underscore using split
temp = a_string.split('_')
# joining result
result = temp[0] + ''.join(ele.title() for ele in temp[1:])
return result
def camel_to_snake(a_string):
"""convert camelCase to snake_case"""
return ''.join(['_'+i.lower() if i.isupper() else i for i in a_string]).lstrip('_')

2
proto

Submodule proto updated: 18fc4cdb52...d7b2791b7c

View File

@@ -12,7 +12,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work
setup(
name="meshtastic",
version="1.2.54",
version="1.2.55",
description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description,
long_description_content_type="text/markdown",