Skip to content

Commit

Permalink
Merge pull request #2 from phasewalker18/DEV_Compatibility
Browse files Browse the repository at this point in the history
Dev compatibility pull to Release for v1.1.
  • Loading branch information
phasewalker18 authored Mar 15, 2018
2 parents 6ffd670 + 897e737 commit 09ab77c
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 994 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ ENV/

# mypy
.mypy_cache/
.vscode/launch.json
97 changes: 81 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,61 @@ It will translate between the binary protocol of the desk and OSC messages to ex

Some basic stateful mode handling is provided to receive text from the DAW and display it on the scribble strips, and deal with issues like fader command echos.

You will need super user (or elevated Administrator in Windows) privileges to use this software, as it uses libpcap packet capture library to establish network connectivity with the Control24 ethernet interface. All other TCP and UDP traffic is ignored/filtered out, so you should not have any privacy concerns if the source has not been tampered with.


### Installing - OSX, macos, Linux

Ensure the current or default python environment has a 2.x interpreter in the current path, and install the pre-requisites into user environment using pip or similar

Example pip install

```
pip install -r requirements.txt --user
```

By default all log outputs will be created into a subdirectory below wherever you install the files, so choose somewhere that this can happen without issues

### Installing - Windows 10

The installation process for Windows is quite a bit more involved, as the OS does not come supplied with several requirements:

* Download and install Python 2.7.x - https://www.python.org/downloads
* Download and install Npcap ensuring to tick the WinPcap API-compatible mode which is off by default - https://nmap.org/npcap/
* Download and install the c++ compiler for python - https://www.microsoft.com/en-us/download/details.aspx?id=44266
* Download the sources from github for: pypcap - https://github.com/pynetwork/pypcap/releases - currently 1.2.0

(Following is a re-statement of the procedure for 'installation from sources' of pypcap found at - https://github.com/pynetwork/pypcap/blob/master/docs/index.rst)

Choose a folder to work in: The 'install' subfolder of ReaControl24 is a reasonable choice.

* Unzip the pypcap download into the chosen install folder.
* Check if the zip made 2 folders called 'pypcap-1.2.0' or similar, one within the other. If so, move the inner one down a level so it sits under 'install'
* Unzip the npcap sdk download. Again see if this results in an inner folder and if so, move it down.
* Rename the folder for this: "wpdpack"
* Start a windows command prompt from the start menu or run "cmd"
* use the CD command to get to the pypcap sources directory you just unzipped, then run the command as follows:

```
C:\Users\Public\Downloads\ReaControl24\install\pypcap-1.2.0> python setup.py install
```

* a lot of output will scroll up the screen, but towards the end should be shown:

```
Installed c:\python27\lib\site-packages\pypcap-1.2.0-py2.7-win-amd64.egg
```

* Now return to the main installation instructions to perform this command:

```
pip install -r requirements.txt -U
```

When complete, to run the daemon, rather than using 'sudo', use an 'Administrator command prompt' and omit the sudo
When supplying a network name, either the name or the GUID will work


## Getting Started

Copy the files to your system in a reasonable spot (your REAPER Scripts directory for example) where you will be able to run the python programs and log files can be created.
Expand All @@ -18,7 +73,7 @@ Copy the provided Reaper.OSC file into the correct directory on your system for
Start REAPER DAW and configure the Control Surface OSC Plugin. Use your local IP address (not localhost or 0.0.0.0)
Set ports as client 9124 and listener 9125.

Start the deamon process with (yes you DO need sudo, see below):
Start the deamon process with (yes you DO need sudo, or for windows omit sudo and use Administrator command prompt):

```
sudo python control24d.py
Expand All @@ -33,10 +88,10 @@ python control24osc.py
### Prerequisites

```
Python 2.x
Python 2.7.x
netifaces
pyOSC
pypcap
pypcap (build from source)
OSC capable DAW such as Reaper 5.x
```
Expand All @@ -47,18 +102,18 @@ Also, the winpcapy library (re-distributed here for now, until a repostiory is f
winpcapy.py, Authored by (c) Massimo Ciani 2009
```

### Installing

You will need super user privileges to use this software, as it uses PCAP to establish network connectivity with the Control24 ethernet interface. All other TCP and UDP traffic is ignored/filtered out, so you should not have any privacy concerns if the source has not been tampered with.
Ensure the current or default python environment has a 2.x interpreter in the current path, and install the pre-requisites into user environment using pip or similar
### Compatibility

Example pip install
Although ReaControl24 is written in python, it depends on certain libraries like pypcap, that are usually wrappers around C libraries. These can vary from platform to platform. Testing of various platforms is ongoing, status at this time is:

```
pip install -r requirements.txt --user
```

By default all log outputs will be created into a subdirectory below wherever you install the files, so choose somewhere that this can happen without issues
|Platform|control24d|control24osc|
|---|---|---|
|macos 10.13.x|Full|Full|
|macos < 10.13|OK in theory|OK in theory|
|Rasbpian ?|Full|Full|
|Other Linux|OK in theory|OK in theory|
|Windows 10|In Progress (DEV_Compatibility branch)|Full|


## Usage
Expand All @@ -73,7 +128,8 @@ All this can be changed by use of command line parameters. Use the --help switch
python control24d.py --help
```

The repo was developed for OSX but in theory, being python, should be portable to other platforms. Please test and report your results.
To exit either process, press CTRL+C on the keyboard in the shell window, or send the process a SIGINT.
In Windows, close the Command Prompt window where you launched the program.

## Running the tests

Expand All @@ -91,8 +147,15 @@ The daemon process MUST be on a host with an interface in the same LAN segment a
## Customisation

A starting Reaper.OSC file is provided with some basic mappings to the OSC address schema. Feel free to add to this as required by your preferences or any new good mappings. Please share (by commit to this repo) anything that proves useful.
The schema is determined by the control24map.py file, each 'address' attribute being appended to the path for the relevante control.
Use the attribute 'CmdClass' to identify the python class that will define the handler for the control. In this way you can implement more complex logic in a python class over and above the 'duh send this address' default.

To make a new mapping, check out the help text in the Default Reaper.OSC file provided by Cockos
Add lines with the token at the start, then followed by the OSC address pattern.

The schema (i.e. the OSC addresses generated by the control24osc.py) is determined by the control24map.py file, each 'address' attribute being appended to the path for the relevant control.
One of the easiest ways to find an address is run the OSC client with the debug switch added, then press the button or control. The address and other information will be appended to the log.

For an entry in the control24map.py, you can use the attribute 'CmdClass' to identify the python class that will define the handler for the control. In this way you can implement more complex logic in a python class over and above the 'duh send this address' default. This is faders, scribble strips etc. are set up already, so that pattern can be followed.

Other attributes determine how the tree is 'walked' according to the binary received from the desk. Byte numbers are zero origin, the first denotes the actual command:
ChildByte which byte to look up to find the next child
ChildByteMask apply this 8 bit mask before lookup
Expand All @@ -107,6 +170,8 @@ Other attributes determine how the tree is 'walked' according to the binary rece
This is freeware, non warranty, non commercial code to benefit the hungry children and hungrier DAW users of the world. If you pull and don't contribute, you should feel bad. Real bad.
Please develop here in this repo for the benefit of all. All pull and merge requests will be accepted and best efforts made to make sense of it all when merging.

Welcome to the latest contributors and collaborators! Your help is very much appreciated.

## Versioning

We will attempt to use [SemVer](http://semver.org/) for versioning. For the versions available, see the tags on this repository.
Expand All @@ -118,7 +183,7 @@ We will attempt to use [SemVer](http://semver.org/) for versioning. For the vers

If you are feeling especially thankful for this entering your life, please feel free to send donations to this BTC address: 1BPQvQjcAGuMjBnG25wuoD64i7KmWZRrpnN

See also the list of contributors
See also the list of contributors via github.

## License

Expand Down
153 changes: 121 additions & 32 deletions control24common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
import optparse
import os
import time
import sys

import netifaces

if sys.platform.startswith('win'):
import _winreg as wr #pylint: disable=E0401

'''
This file is part of ReaControl24. Control Surface Middleware.
Copyright (C) 2018 PhaseWalker
Expand Down Expand Up @@ -118,41 +122,126 @@ def opts_common(desc):
return oprs


def format_ip(ipaddr, port):
"""from ip and port provide a string with ip:port"""
return '{}:{}'.format(ipaddr, port)


def ipv4(interface_name=None):
""" Get an IPv4 address plausible default. """
# to not take into account loopback addresses (no interest here)
ifaces = netifaces.interfaces()
default_interface = DEFAULTS.get('interface')
default_address = DEFAULTS.get('ip')
found = False
if not interface_name is None and not interface_name in ifaces:
raise KeyError('%s is not a valid interface name' % interface_name)
for interface in ifaces:
if interface_name is None or unicode(interface_name) == interface:
config = netifaces.ifaddresses(interface)
# AF_INET is not always present
if netifaces.AF_INET in config.keys():
for link in config[netifaces.AF_INET]:
# loopback holds a 'peer' instead of a 'broadcast' address
if 'addr' in link.keys() and 'peer' not in link.keys():
default_interface = interface
default_address = link['addr']
found = True
break

if not interface_name is None and not found:
raise LookupError(
'%s interface has no ipv4 addresses' % interface_name)

return default_interface, default_address

def hexl(inp):
"""Convert to hex string using binascii but
then pretty it up by spacing the groups"""
shex = binascii.hexlify(inp)
return ' '.join([shex[i:i+2] for i in range(0, len(shex), 2)])



class NetworkHelper(object):
"""class to contain network related helpful methods
and such to be re-used where needed"""
def __init__(self):
self.networks = NetworkHelper.list_networks()

def __str__(self):
"""return a nice list"""
return '\n'.join(['{} {}'.format(key, data.get('name') or '') for key, data in self.networks.iteritems()])

def get_default(self):
"""return the name and first ip of whichever adapter
is marked as default"""
default = [key for key, data in self.networks.iteritems() if data.has_key('default')]
if default:
def_net = default[0]
def_ip = self.networks[def_net].get('ip')[0].get('addr')
return def_net, def_ip
return None

def get(self, name):
"""get the full entry for a network by name
but also look by friendly name if not an adapter name"""
if self.networks.has_key(name):
return self.networks[name]
results = [key for key, data in self.networks.iteritems() if data.get('name') == name]
if results:
return self.networks[results[0]]
return None

def verify_ip(self, ipstr):
"""search for an adapter that has the ip address supplied"""
for key, data in self.networks.iteritems():
if data.has_key('ip'):
for ip in data['ip']:
if ip.get('addr') == ipstr:
return key
return None

@staticmethod
def get_ip_address(ifname):
"""Use netifaces to retrieve ip address, but handle if it doesn't exist"""
try:
addr_l = netifaces.ifaddresses(ifname)[netifaces.AF_INET]
return [{k: v.encode('ascii', 'ignore') for k, v in addr.iteritems()} for addr in addr_l]
except KeyError:
return None

@staticmethod
def get_mac_address(ifname):
"""Use netifaces to retrieve mac address, but handle if it doesn't exist"""
try:
addr_l = netifaces.ifaddresses(ifname)[netifaces.AF_LINK]
addr = addr_l[0].get('addr')
return addr.encode('ascii', 'ignore')
except KeyError:
return None

@staticmethod
def list_networks_win(networks):
"""Windows shim for list_networks. Also go to the registry to
get a friendly name"""
reg = wr.ConnectRegistry(None, wr.HKEY_LOCAL_MACHINE)
reg_key = wr.OpenKey(
reg,
r'SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}'
)
for key, val in networks.iteritems():
val['pcapname'] = '\\Device\\NPF_{}'.format(key)
net_regkey = r'{}\Connection'.format(key)
try:
net_key = wr.OpenKey(reg_key, net_regkey)
net_name = wr.QueryValueEx(net_key, 'Name')[0]
if net_name:
val['name'] = net_name
except WindowsError: #pylint: disable=E0602
pass
wr.CloseKey(reg_key)
return networks

@staticmethod
def list_networks():
"""Gather networks info via netifaces library"""
default_not_found = True
names = [a.encode('ascii', 'ignore') for a in netifaces.interfaces()]
results = {}
for interface in names:
inner = {
'pcapname': interface,
'mac': NetworkHelper.get_mac_address(interface)
}
#ip
ips = NetworkHelper.get_ip_address(interface)
if ips:
inner['ip'] = ips
if default_not_found and any([ip.has_key('addr') and not ip.has_key('peer') for ip in ips]):
default_not_found = False
inner['default'] = True
results[interface] = inner
if sys.platform.startswith('win'):
return NetworkHelper.list_networks_win(results)
return results

@staticmethod
def ipstr_to_tuple(ipstr):
ipsplit = ipstr.split(':')
return (ipsplit[0], int(ipsplit[1]))

@staticmethod
def ipstr_from_tuple(ipaddr, ipport):
"""from ip and port provide a string with ip:port"""
return '{}:{}'.format(ipaddr, ipport)


Loading

0 comments on commit 09ab77c

Please sign in to comment.