Created
May 30, 2018 23:31
-
-
Save branw/42596cf7d66c779d1ac044a54de58660 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
classdef BatBot | |
% BATBOT is a client for NI-based BatBots | |
% | |
% PROPERTIES | |
% - VERSION (const) - Application version number | |
% - Hostname - Network address of the server | |
% | |
% METHODS | |
% - collectData - Run a task with given parameters and return the | |
% collected data | |
% | |
% EXAMPLES | |
% - Connect to the bot named 'sonar' and run an 'echo' task with its | |
% 'interpulse_time' parameter set to 12: | |
% bot = BatBot('sonar'); | |
% data = bot.collectData('echo', 'interpulse_time', 12); | |
% | |
% - Connect to the bot at IP 192.168.1.110 and run the 'test' task: | |
% bot = BatBot('192.168.1.110'); | |
% test = bot.collectData('test') | |
properties (Constant) | |
% Application version number (should be same between client/server) | |
VERSION = [0 1]; | |
end | |
properties (Constant, Access=private) | |
% Time to wait for responses to discovery request | |
BROADCAST_TIMEOUT = 1; | |
% Network broadcast address | |
BROADCAST_ADDR = '192.168.1.255'; | |
% Port to broadcast discovery request | |
BROADCAST_SEND_PORT = 45454; | |
% Port to receive broadcasted discovery responses | |
BROADCAST_RECV_PORT = 45455; | |
end | |
properties | |
% Network address of the server | |
Hostname | |
end | |
methods | |
%% BATBOT | |
function obj = BatBot(host, varargin) | |
% BATBOT creates a client instance that is connected to a | |
% server | |
% | |
% SYNOPSIS | |
% Discovers bots on the local network and establishes a | |
% connection to a given bot's server | |
% | |
% EXAMPLES | |
% Connect to the bot nicknamed 'sonar': | |
% bot = BatBot('sonar'); | |
% | |
% Connect to the bot with the IP '192.168.1.120': | |
% bot = BatBot('192.168.1.120'); | |
% Validate function inputs | |
parser = inputParser; | |
parser.FunctionName = 'BatBot'; | |
parser.addRequired('host', @ischar); | |
parser.parse(host, varargin{:}); | |
% Check if an IP address is given | |
% Horrific regex from https://www.regular-expressions.info/ip.html | |
if regexp(host, '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$') | |
obj.Hostname = host; | |
% Otherwise, hope a nickname given | |
else | |
% Run a discovery and get the IP by the name | |
[ips, names] = obj.discoverPeers(); | |
index = find(strcmpi(names, host)); | |
% No matching name was found | |
if isempty(index) | |
throw(MException('BatBot:BatBot', 'Unable to find bot by name')); | |
end | |
obj.Hostname = ips(index); | |
end | |
% Check that we can connect to the server | |
info = obj.getInfo(); | |
% Check that the version numbers match | |
if info.versionMajor ~= obj.VERSION(1) || info.versionMinor ~= obj.VERSION(2) | |
throw(MException('BatBot:BatBot', 'The BatBot is running a different version')); | |
end | |
end | |
%% COLLECTDATA | |
function data = collectData(obj, taskName, varargin) | |
% COLLECTDATA automatically runs a task and returns the | |
% collected data | |
% | |
% SYNOPSIS | |
% data = collectData(taskName [, param1Name, param1Key, ...]) | |
% taskName - name of task to run | |
% params - optional name-value pairs of parameters to pass | |
% | |
% EXAMPLES | |
% Run the task named 'echo' with default parameters: | |
% data = collectData('echo') | |
% | |
% Do the same but with the 'amplitude' parameter set to 3000: | |
% data = collectData('echo', 'amplitude', 3000) | |
% If no params were passed, put an empty string to make | |
% webwrite happy | |
if size(varargin) == 0 | |
varargin = {''}; | |
end | |
try | |
data = webwrite(sprintf('http://%s:8080/BatBot/CollectData?%s', obj.Hostname, taskName), varargin{:}); | |
catch | |
throw(MException('BatBot:BatBot', 'Unable to begin data collection')); | |
end | |
end | |
end | |
methods (Access=private) | |
%% GETINFO | |
function info = getInfo(obj) | |
% GETINFO returns server status information | |
try | |
info = webread(sprintf('http://%s:8080/BatBot/Info', obj.Hostname)); | |
catch | |
throw(MException('BatBot:BatBot', 'Unable to poll server status')); | |
end | |
end | |
end | |
methods (Static, Access=private) | |
%% DISCOVERPEERS | |
function [ips, names] = discoverPeers() | |
% DISCOVERPEERS discovers peers through a UDP broadcast | |
% protocol | |
% Open socket on broadcast to send/receive with different ports | |
sock = udp(BatBot.BROADCAST_ADDR, BatBot.BROADCAST_SEND_PORT,... | |
'LocalPort', BatBot.BROADCAST_RECV_PORT,... | |
'EnablePortSharing', 'on'); | |
fopen(sock); | |
% Send discovery request | |
fwrite(sock, ['BATBOT' BatBot.VERSION]); | |
ips = []; | |
names = []; | |
% Loop until the timeout | |
tic() | |
while toc() < BatBot.BROADCAST_TIMEOUT | |
% Wait for data | |
if sock.BytesAvailable > 0 | |
% Read a datagram and it's sender's address | |
[data, ~, ~, addr] = fread(sock); | |
% Check if peer has already been discovered | |
if ~ismember(addr, ips) | |
ips = [ips string(addr)]; | |
% Verify magic | |
if ~strcmp(char(data(1:6)'), 'BATBOT') | |
continue | |
end | |
% Verify matching versions | |
if ~isequal(data(7:8)', BatBot.VERSION) | |
continue | |
end | |
% Decode payload | |
len = data(9); | |
name = string(char(data(10:9+len)')); | |
names = [names name]; | |
end | |
end | |
end | |
fclose(sock); | |
end | |
end | |
end | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
classdef Goose < handle & matlab.mixin.CustomDisplay | |
properties (Constant) | |
VERSION = [0 1]; | |
end | |
properties | |
TargetType | |
Target | |
end | |
properties (Access = private) | |
Serial | |
end | |
methods | |
function obj = Goose(varargin) | |
p = inputParser; | |
p.FunctionName = 'Goose'; | |
p.addRequired('target', @isstring); | |
p.parse(varargin{:}); | |
target = p.Results.target; | |
% Check if an IP address is given | |
% Horrific regex from https://www.regular-expressions.info/ip.html | |
if regexp(target, '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$') | |
obj.TargetType = 'Remote'; | |
obj.Target = target; | |
throw(MException('Goose:Goose', 'Remote connections are not implemented :(')); | |
% Check if its a serial port | |
elseif regexpi(target, 'COM([0-9]{0,3})') | |
target = upper(target); | |
obj.TargetType = 'Local'; | |
obj.Target = target; | |
% Check if the port is currently available | |
availablePorts = seriallist; | |
if ~ismember(target, availablePorts) | |
if ~isempty(availablePorts) | |
msg = sprintf('Serial port %s is not available\n\nDid you mean one of these?\n%s', target, strjoin(availablePorts, '\n')); | |
else | |
msg = 'No serial ports are available'; | |
end | |
throw(MException('Goose:notImplemente', msg)); | |
end | |
obj.Serial = serial(target, 'BaudRate', 115200); | |
fopen(obj.Serial); | |
version = obj.hello(); | |
if ~isequal(version, obj.VERSION) | |
msg = sprintf('The device version (v%d.%d) does not match the interface version (v%d.%d)', version, obj.VERSION); | |
throw(MException('Goose:versionMismatch', msg)); | |
end | |
% Assume its a nickname on the local network | |
else | |
obj.TargetType = 'Remote'; | |
throw(MException('Goose:notImplemented', 'Local network discovery is not implemented :(\n\n(If you weren''t trying to do this, double check that either the IP address or\n serial port was spelt correctly!)')); | |
end | |
end | |
function sess = createSession(obj) | |
sess = Session(obj); | |
end | |
end | |
% Don't let anyone else see these functions! | |
methods (Hidden) | |
% Send a handshake and get back the device version | |
function version = hello(obj) | |
fprintf(obj.Serial, 'hello\n'); | |
fwrite(obj.Serial, obj.VERSION, 'uint8'); | |
reply = fscanf(obj.Serial); | |
if ~strcmp(reply, 'Hello') | |
msg = sprintf('Device reply ("%s") does not match expected ("Hello")', reply); | |
throw(MException('Goose:badReply', msg)); | |
end | |
version = fread(obj.Serial, 2, 'uint8'); | |
end | |
function data = collectData(obj, rate, channels, data) | |
fprintf(obj.Serial, 'collectData\n'); | |
fwrite(obj.Serial, rate, 'uint32'); | |
fwrite(obj.Serial, length(channels), 'uint8'); | |
for ch = [channels; data] | |
fwrite(obj.Serial, ch(1), 'uint8'); | |
fwrite(obj.Serial, length(ch(2)), 'uint32'); | |
fwrite(obj.Serial, ch(2), 'uint32'); | |
end | |
reply = fscanf(obj.Serial); | |
if ~strcmp(reply, 'CollectData') | |
msg = sprintf('Device reply ("%s") does not match expected ("CollectData")', reply); | |
throw(MException('Goose:badReply', msg)); | |
end | |
data = fread(obj.Serial, ); | |
end | |
end | |
methods (Access = private) | |
% Only called when there are no more sessions alive | |
function delete(obj) | |
fclose(obj.Serial); | |
end | |
end | |
methods (Access = protected) | |
% Display a nice output message a la NI | |
function displayScalarObject(obj) | |
className = matlab.mixin.CustomDisplay.getClassNameForHeader(obj); | |
if obj.TargetType == "Local" | |
connection = sprintf("locally (port %s)", obj.Target); | |
elseif obj.TargetType == "Remote" | |
connection = "remotely"; | |
end | |
fprintf('Daq Daq %s (v%d.%d) connected %s\n', className, obj.VERSION, connection); | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
classdef Session < handle | |
properties | |
Device | |
Channels | |
Fs | |
end | |
methods | |
function obj = Session(varargin) | |
p = inputParser; | |
p.FunctionName = 'Session'; | |
p.addRequired('device', @(x) isa(x, 'Goose')); | |
p.parse(varargin{:}); | |
obj.Device = p.Results.device; | |
end | |
function ch = addAnalogOutputChannel(obj, varargin) | |
obj.Channels = [obj.Channels ]; | |
end | |
function ch = addAnalogInputChannel(obj, varargin) | |
end | |
function ch = addDigitalOutputChannel(obj, varargin) | |
throw(MException('Goose:Session:notImplemented', 'Digital output channels are not implemented yet :(')); | |
end | |
function ch = addDigitalInputChannel(obj, varargin) | |
throw(MException('Goose:Session:notImplemented', 'Digital input channels are not implemented yet :(')); | |
end | |
function queueOutputData(obj, data) | |
end | |
function data = collectData(obj) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment