Skip to content

Instantly share code, notes, and snippets.

@branw
Created May 30, 2018 23:31
Show Gist options
  • Save branw/42596cf7d66c779d1ac044a54de58660 to your computer and use it in GitHub Desktop.
Save branw/42596cf7d66c779d1ac044a54de58660 to your computer and use it in GitHub Desktop.
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
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
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