Browse Source

+ ActuBoard platform initial public Open Source commit

SG 3 years ago
commit
8d01d28dfb
33 changed files with 2625 additions and 0 deletions
  1. 6 0
      .gitignore
  2. 0 0
      ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/db.lock
  3. BIN
      ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide
  4. BIN
      ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide-shm
  5. BIN
      ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide-wal
  6. 93 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardApp.xaml
  7. 274 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardApp.xaml.cs
  8. 110 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardConsole.csproj
  9. 6 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.config
  10. 8 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.xaml
  11. 17 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.xaml.cs
  12. 55 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/AssemblyInfo.cs
  13. 71 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Resources.Designer.cs
  14. 117 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Resources.resx
  15. 30 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Settings.Designer.cs
  16. 7 0
      ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Settings.settings
  17. 28 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface.sln
  18. 60 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/ActuBoardAPI.csproj
  19. 238 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/ActuBoardInterface.cs
  20. 44 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/Helper.cs
  21. 38 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/IActuBoard.cs
  22. 36 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/Properties/AssemblyInfo.cs
  23. 17 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/exceptions/ActuBoardException.cs
  24. 19 0
      ActuBoard_API/ActuBoardInterface/ActuBoardInterface/exceptions/StatusCode.cs
  25. 97 0
      ActuBoard_Firmware/ActuBoard/ActuBoard.ino
  26. 625 0
      ActuBoard_Firmware/ActuBoard/cmdInterpreter.cpp
  27. 47 0
      ActuBoard_Firmware/ActuBoard/cmdInterpreter.h
  28. 95 0
      ActuBoard_Firmware/ActuBoard/doubleSerial.cpp
  29. 33 0
      ActuBoard_Firmware/ActuBoard/doubleSerial.h
  30. 382 0
      ActuBoard_Firmware/ActuBoard/pca9635.cpp
  31. 58 0
      ActuBoard_Firmware/ActuBoard/pca9635.h
  32. 14 0
      README.md
  33. BIN
      Schematics/Commands_ActuBoard.xlsx

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+*.suo
+*.user
+_ReSharper.*
+bin
+obj
+packages

+ 0 - 0
ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/db.lock


BIN
ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide


BIN
ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide-shm


BIN
ActuBoard_API/ActuBoardInterface/.vs/ActuBoardInterface/v15/Server/sqlite3/storage.ide-wal


+ 93 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardApp.xaml

@@ -0,0 +1,93 @@
+<Window x:Class="ActuBoardConsole.ActuBoardApp"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:ActuBoardConsole"
+        mc:Ignorable="d"
+        Title="ActuBoard Console" Height="560" Width="720"
+        Background="#FF111111">
+    <Window.Resources>
+        <Style TargetType="{x:Type Label}">
+            <Setter Property="Control.Foreground" Value="Gray"/>
+        </Style>
+    </Window.Resources>
+
+    <Grid>
+
+        <Border Padding="10">
+        <StackPanel>
+            <Grid Margin="10">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="10" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+                <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="90" />
+                        <ColumnDefinition Width="150" />
+                        <ColumnDefinition Width="100" />
+                        <ColumnDefinition Width="*" />
+                        <ColumnDefinition Width="50" />
+                    </Grid.ColumnDefinitions>
+
+                <Label Grid.Row="0" Grid.Column="0" x:Name="lbl_com" Content="COM" HorizontalAlignment="Right" />
+                    <ComboBox Grid.Row="0" Grid.Column="1" x:Name="drop_com" HorizontalAlignment="Left" Width="130"/>
+                    <Button x:Name="btn_connect" Grid.Row="0" Grid.Column="2" Content="Connect" HorizontalAlignment="Left" Width="75"/>
+                    <Button x:Name="btn_output" Grid.Row="0" Grid.Column="3" Content="Toggle Output" HorizontalAlignment="Left" Width="90"/>
+
+                    <Button x:Name="btn_debug" HorizontalAlignment="Left" Width="100" Margin="110,0,0,0" Grid.Column="3" Grid.Row="0" Content="Toggle Debug" />
+
+                    <Ellipse x:Name="el_status" Grid.Row="0" Grid.Column="4" Fill="Gray" Height="20" Width="20"/>
+
+
+                    <Label Grid.Row="2" Grid.Column="0" x:Name="lbl_command" Content="Command" HorizontalAlignment="Right"/>
+                    <TextBox x:Name="txt_command" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" Text="" Width="130" KeyDown="txt_command_KeyDown"/>
+                    <Button x:Name="btn_send" Grid.Row="2" Grid.Column="2" HorizontalAlignment="Left" Content="Send" Width="75" />
+                    <Label Grid.Row="2" Grid.Column="3" x:Name="lbl_hexinfo" Content="(only hex values!)" HorizontalAlignment="Left" FontStyle="Italic" FontSize="10" />
+                    <Ellipse x:Name="el_error" Grid.Row="2" Grid.Column="4" Fill="Black" Height="15" Width="15"/>
+                </Grid>
+
+
+            <Grid Margin="10">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="25" />
+                    <RowDefinition Height="10" />
+                    <RowDefinition Height="25" />
+                </Grid.RowDefinitions>
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="90" />
+                    <ColumnDefinition Width="120" />
+                    <ColumnDefinition Width="120" />
+                    <ColumnDefinition Width="120" />
+                    <ColumnDefinition Width="*" />
+                </Grid.ColumnDefinitions>
+
+                <!-- Predefined commands -->
+                    <Button x:Name="btn_list" HorizontalAlignment="Left" Width="100" Grid.Column="4" Grid.Row="0" Content="List Devices" />
+                    <Label Grid.Row="0" Grid.Column="0" x:Name="lbl_read" Content="Channel (Int)" HorizontalAlignment="Right"/>
+                    <TextBox x:Name="txt_channel" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" Text="" Width="40" KeyDown="txt_command_KeyDown"/>
+                    <Button x:Name="btn_read" HorizontalAlignment="Right" Margin="0,0,20,0" Width="50" Grid.Column="1" Grid.Row="0" Content="Read" />
+
+
+                    <Label Grid.Row="0" Grid.Column="2" x:Name="lbl_value" Content="Value (Int: 0-255)" HorizontalAlignment="Right"/>
+                    <TextBox x:Name="txt_value" Grid.Row="0" Grid.Column="3" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" Text="" Width="40" />
+                    <Button x:Name="btn_send_value" HorizontalAlignment="Right" Margin="0,0,20,0" Width="50" Grid.Column="3" Grid.Row="0" Content="Send" />
+
+
+
+                    <Button x:Name="btn_test" HorizontalAlignment="Left" Width="100" Grid.Column="1" Grid.Row="3" Content="Test wrong cmd" />
+                    <Button x:Name="btn_group_m" HorizontalAlignment="Left" Width="100" Grid.Column="2" Grid.Row="3" Content="Test Group (m)" />
+                    <Button x:Name="btn_group_n" HorizontalAlignment="Left" Width="100" Grid.Column="3" Grid.Row="3" Content="Test Group (n)" />
+
+                </Grid>
+
+                <Label x:Name="lbl_console" Content="Console Output" HorizontalAlignment="Left" Margin="20,0,20,0"/>
+                <TextBox x:Name="txt_console" Height="310" Padding="5" Margin="20,0,20,20" AcceptsReturn="True" TextWrapping="Wrap" Text="" Background="Black" Foreground="LimeGreen" FontFamily="Courier New" FontSize="12" />
+
+
+
+            </StackPanel>
+    </Border>
+    </Grid>
+</Window>

+ 274 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardApp.xaml.cs

@@ -0,0 +1,274 @@
+using ActuBoardAPI;
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace ActuBoardConsole
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    public partial class ActuBoardApp : Window
+    {
+
+        private ActuBoardInterface _abi;
+        private int _comport = 0;
+        private bool _outputEnabled = true;
+
+        public ActuBoardApp()
+        {
+            InitializeComponent();
+
+
+            foreach (String port in SerialPort.GetPortNames())
+            {
+                drop_com.Items.Add(port);
+            }
+
+
+            _abi = new ActuBoardInterface();
+            _abi.OnConnectionChanged += _abi_OnConnectionChanged;
+            _abi.OnMessageReceived += _abi_OnMessageReceived;
+            _abi.OnStatusUpdate += _abi_OnStatusUpdate;
+
+            drop_com.SelectionChanged += Drop_com_SelectionChanged;
+            if (drop_com.Items.Count > 0)
+                drop_com.SelectedIndex = 0;
+
+            btn_connect.Click += Btn_connect_Click;
+
+            btn_list.Click += Btn_list_Click;
+            btn_send.Click += Btn_send_Click;
+            btn_read.Click += Btn_read_Click;
+            btn_debug.Click += Btn_debug_Click;
+            btn_output.Click += Btn_output_Click;
+            btn_send_value.Click += Btn_send_value_Click;
+
+            btn_group_m.Click += Btn_group_m_Click;
+            btn_group_n.Click += Btn_group_n_Click;
+
+            btn_test.Click += Btn_test_Click;
+
+        }
+
+        private void Btn_send_value_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected() && txt_channel.Text != "")
+            {
+                int channel = 0;
+                Int32.TryParse(txt_channel.Text, out channel);
+                int value = 0;
+                Int32.TryParse(txt_value.Text, out value);
+                if (value > 255 || value < 0)
+                    value = 0;
+                _abi.SetChannel(channel, value);
+            }
+        }
+
+        private void Btn_group_n_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+            {
+                int[] channels = { 30, 31, 32 };
+                int[] pwm = { 128, 15, 255 };
+                _abi.SetChannels(channels, pwm);
+            }
+        }
+
+        private void Btn_group_m_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+            {
+                int[] channels = { 30, 31, 32 };
+                _abi.SetChannels(channels, 128);
+            }
+        }
+
+        private void _abi_OnStatusUpdate(StatusCode e)
+        {
+            Dispatcher.BeginInvoke(new Action(delegate ()
+            {
+                if (e > 0)
+                    Console.Beep();
+
+                // error released
+                if (e == StatusCode.NoError)
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.LimeGreen);
+                }
+                // hardware controller could not interpret command
+                else if (e == StatusCode.HardwareCommandError)
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.Red);
+                }
+                // selected channel(s) are invalid or out of range
+                else if (e == StatusCode.ChannelInvalid)
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.BlueViolet);
+                }
+                // pwm value(s) are out of range (0-255)
+                else if (e == StatusCode.PwmValueInvalid)
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.Coral);
+                }
+                // hardware is not connected
+                else if (e == StatusCode.ActuBoardNotConnected)
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.DarkRed);
+                }
+                // unknown error ID
+                else
+                {
+                    el_error.Fill = new SolidColorBrush(Colors.Red);
+                }
+            }));
+        }
+
+        private void Btn_test_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+            {
+                _abi.SetChannel("30", "240");
+            }
+        }
+
+        private void Btn_output_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+            {
+                _abi.EnableOutput(!_outputEnabled);
+                _outputEnabled = !_outputEnabled;
+                if (_outputEnabled)
+                {
+                    SetStatusColor(1);
+                    btn_output.Content = "disable output";
+                }
+                else
+                {
+                    SetStatusColor(3);
+                    btn_output.Content = "enable output";
+                }
+            }
+        }
+
+        private void Btn_debug_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+                _abi.ToggleDebug();
+        }
+
+        private void Btn_read_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected() && txt_channel.Text != "")
+            {
+                int channel = 0;
+                Int32.TryParse(txt_channel.Text, out channel);
+                _abi.GetChannel(channel);
+            }
+        }
+
+        private void Btn_send_Click(object sender, RoutedEventArgs e)
+        {
+
+            if (_abi.IsConnected() && txt_command.Text != "")
+            {
+                _abi.SendCustomCommand(txt_command.Text);
+            }
+        }
+
+        private void Btn_list_Click(object sender, RoutedEventArgs e)
+        {
+            if (_abi.IsConnected())
+                _abi.ListDevices();
+        }
+
+        private void _abi_OnMessageReceived(string e)
+        {
+            Console.WriteLine("MSG: " + e);
+            Dispatcher.BeginInvoke(new Action(delegate ()
+            {
+                txt_console.Text += e;
+                txt_console.ScrollToEnd();
+            }));
+        }
+
+        private void _abi_OnConnectionChanged(bool e)
+        {
+            if (e)
+            {
+                SetStatusColor(1);
+                btn_connect.Content = "Disconnect";
+            }
+            else
+            {
+                SetStatusColor(2);
+                btn_connect.Content = "Connect";
+            }
+        }
+
+
+        private void SetStatusColor(int status)
+        {
+            // unknown status
+            if (status == 0)
+                el_status.Fill = new SolidColorBrush(Colors.Black);
+            // connected
+            else if (status == 1)
+                el_status.Fill = new SolidColorBrush(Colors.LimeGreen);
+            // disconnected
+            else if (status == 2)
+                el_status.Fill = new SolidColorBrush(Colors.Red);
+            // output disabled
+            else if (status == 3)
+                el_status.Fill = new SolidColorBrush(Colors.GreenYellow);
+        }
+
+
+        private void Drop_com_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            _comport = ComToInt(drop_com.SelectedItem.ToString());
+            _abi.SetComPort(_comport);
+
+        }
+
+
+        private int ComToInt(String scom)
+        {
+            Console.WriteLine("COMPORT: " + scom);
+            if (scom.StartsWith("COM"))
+                return Int32.Parse(scom.Substring(3));
+            else
+                return 0;
+        }
+
+        private void Btn_connect_Click(object sender, RoutedEventArgs e)
+        {
+            Console.WriteLine("Connecting to " + _comport);
+            if (_comport > 0 && !_abi.IsConnected())
+                _abi.Connect();
+            else if (_abi.IsConnected())
+                _abi.Disconnect();
+        }
+
+        private void txt_command_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.Enter)
+            {
+                Console.WriteLine("sender " + sender.ToString());
+                btn_send.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
+            }
+        }
+    }
+}

+ 110 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/ActuBoardConsole.csproj

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{AA5A877B-5C89-455E-A63F-01B818613C3C}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>ActuBoardConsole</RootNamespace>
+    <AssemblyName>ActuBoardConsole</AssemblyName>
+    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <WarningLevel>4</WarningLevel>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xaml" />
+    <Reference Include="System.Xml" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="WindowsBase" />
+    <Reference Include="PresentationCore" />
+    <Reference Include="PresentationFramework" />
+  </ItemGroup>
+  <ItemGroup>
+    <ApplicationDefinition Include="App.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </ApplicationDefinition>
+    <Page Include="ActuBoardApp.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
+    <Compile Include="App.xaml.cs">
+      <DependentUpon>App.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Include="ActuBoardApp.xaml.cs">
+      <DependentUpon>ActuBoardApp.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Properties\AssemblyInfo.cs">
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DesignTime>True</DesignTime>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <Compile Include="Properties\Settings.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Settings.settings</DependentUpon>
+      <DesignTimeSharedInput>True</DesignTimeSharedInput>
+    </Compile>
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+    </EmbeddedResource>
+    <None Include="Properties\Settings.settings">
+      <Generator>SettingsSingleFileGenerator</Generator>
+      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+    </None>
+    <AppDesigner Include="Properties\" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\ActuBoardInterface\ActuBoardAPI.csproj">
+      <Project>{22CDAA71-52C6-486E-B6AE-F459A47350A1}</Project>
+      <Name>ActuBoardAPI</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 6 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
+    </startup>
+</configuration>

+ 8 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.xaml

@@ -0,0 +1,8 @@
+<Application x:Class="ActuBoardConsole.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:ActuBoardConsole"
+             StartupUri="ActuBoardApp.xaml">
+    <Application.Resources>
+    </Application.Resources>
+</Application>

+ 17 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/App.xaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace ActuBoardConsole
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 55 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/AssemblyInfo.cs

@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ActuBoardConsole")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ActuBoardConsole")]
+[assembly: AssemblyCopyright("Copyright ©  2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set 
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>.  For example, if you are using US english
+//in your source files, set the <UICulture> to en-US.  Then uncomment
+//the NeutralResourceLanguage attribute below.  Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page, 
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page, 
+                                              // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 71 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Resources.Designer.cs

@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace ActuBoardConsole.Properties
+{
+
+
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if ((resourceMan == null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ActuBoardConsole.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+    }
+}

+ 117 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Resources.resx

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>

+ 30 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Settings.Designer.cs

@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace ActuBoardConsole.Properties
+{
+
+
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+    {
+
+        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+        public static Settings Default
+        {
+            get
+            {
+                return defaultInstance;
+            }
+        }
+    }
+}

+ 7 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardConsole/Properties/Settings.settings

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
+  <Profiles>
+    <Profile Name="(Default)" />
+  </Profiles>
+  <Settings />
+</SettingsFile>

+ 28 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface.sln

@@ -0,0 +1,28 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActuBoardAPI", "ActuBoardInterface\ActuBoardAPI.csproj", "{22CDAA71-52C6-486E-B6AE-F459A47350A1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActuBoardConsole", "ActuBoardConsole\ActuBoardConsole.csproj", "{AA5A877B-5C89-455E-A63F-01B818613C3C}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{22CDAA71-52C6-486E-B6AE-F459A47350A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{22CDAA71-52C6-486E-B6AE-F459A47350A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{22CDAA71-52C6-486E-B6AE-F459A47350A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{22CDAA71-52C6-486E-B6AE-F459A47350A1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AA5A877B-5C89-455E-A63F-01B818613C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AA5A877B-5C89-455E-A63F-01B818613C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AA5A877B-5C89-455E-A63F-01B818613C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AA5A877B-5C89-455E-A63F-01B818613C3C}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

+ 60 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/ActuBoardAPI.csproj

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{22CDAA71-52C6-486E-B6AE-F459A47350A1}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>ActuBoardInterface</RootNamespace>
+    <AssemblyName>ActuBoardInterface</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <TargetFrameworkProfile>Unity Full v3.5</TargetFrameworkProfile>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+    <Reference Include="WindowsBase" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="exceptions\ActuBoardException.cs" />
+    <Compile Include="ActuBoardInterface.cs" />
+    <Compile Include="exceptions\StatusCode.cs" />
+    <Compile Include="Helper.cs" />
+    <Compile Include="IActuBoard.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 238 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/ActuBoardInterface.cs

@@ -0,0 +1,238 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+
+namespace ActuBoardAPI
+{
+
+    public class ActuBoardInterface : IActuBoard
+    {
+
+        public event Action<bool> OnConnectionChanged;
+        public event Action<String> OnMessageReceived;
+        
+        // status event
+        public event Action<StatusCode> OnStatusUpdate;
+
+        private char EOL = '\r';
+        private int _baud = 115200;
+        private int _com = -1;
+
+        private SerialPort _io;
+
+		
+		/**
+		* connect to ActuBoard
+		*/
+        public int Connect()
+        {
+
+            // COM Port not set
+            if (_com <= 0)
+            {
+                OnStatusUpdate?.Invoke(StatusCode.ComPortInvalid);
+                return -1;
+            }
+
+            // start SerialPort connection
+            Console.WriteLine("Connecting to port " + _com);
+            _io = new SerialPort("COM" + _com)
+            {
+                BaudRate = _baud
+            };
+            OnStatusUpdate?.Invoke(StatusCode.ActuBoardIsConnecting);
+            _io.Open();
+
+
+            // if could not open SerialPort set error
+            if (!_io.IsOpen)
+            {
+                //OnStatusUpdate(this, StatusCode.ActuBoardNotConnected);
+                OnStatusUpdate?.Invoke(StatusCode.ActuBoardNotConnected);
+                return -2;
+            }
+
+            OnConnectionChanged?.Invoke(true);
+            //OnConnectionChanged?.Invoke(true); // default .NET
+            OnStatusUpdate?.Invoke(StatusCode.ActuBoardConnected); // Unity .NET
+
+            Console.WriteLine("Connected!");
+            
+            _io.DataReceived += _io_DataReceived;
+
+            return 1;
+        }
+
+        private void _io_DataReceived(object sender, SerialDataReceivedEventArgs e)
+        {
+            int count = _io.BytesToRead;
+            byte[] msg = new byte[count];
+            _io.Read(msg, 0, count);
+            System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
+            String smsg = enc.GetString(msg);
+
+            if (smsg.Contains('\a'))
+                OnStatusUpdate?.Invoke(StatusCode.HardwareCommandError);
+            else
+                OnStatusUpdate?.Invoke(StatusCode.NoError);
+
+            OnMessageReceived?.Invoke(smsg);
+        }
+
+
+        public int Disconnect()
+        {
+            if (_io == null)
+                return -1;
+            if (!_io.IsOpen)
+                return -2;
+
+            _io.Close();
+
+            OnConnectionChanged?.Invoke(false);
+            return 1;
+        }
+
+        public bool IsConnected()
+        {
+            if (_io != null)
+                return _io.IsOpen;
+            else
+                return false;
+        }
+
+
+		/**
+		* enable or disable output of _every_ ActuBoard Hub at once
+		* (e.g., useful as emergency switch)
+		*/
+        public void EnableOutput(bool enable)
+        {
+            if (enable)
+                WriteCommand("O01" + EOL);
+            else
+                WriteCommand("O00" + EOL);
+        }
+
+
+        public int EnableBlinking(int cid)
+        {
+			WriteCommand("G" + Helper.IntToHex(cid) + "01" + EOL);
+			return 0;
+        }
+		
+        public int DisableBlinking(int cid)
+        {
+			WriteCommand("G" + Helper.IntToHex(cid) + "00" + EOL);
+			return 0;
+        }
+		
+        public int EnableBlinking(int[] cid)
+        {
+			// currently not needed, implemented soon
+            throw new NotImplementedException();
+        }
+		
+        public int DisableBlinking(int[] cid)
+        {
+			// currently not needed, implemented soon
+            throw new NotImplementedException();
+        }
+        public void SendCustomCommand(string cmd)
+        {
+            WriteCommand(cmd + EOL);
+        }
+
+
+        public int SetChannel(int cid, int pwm)
+        {
+            SetChannel(Helper.IntToHex(cid), Helper.IntToHex(pwm));
+            return 0;
+        }
+
+        public int SetChannel(string cid, string pwm)
+        {
+            WriteCommand("S" + cid + pwm + EOL);
+            return 0;
+        }
+
+        public int GetChannel(int cid)
+        {
+            WriteCommand("R" + cid.ToString("X2") + EOL);
+            return 0;
+        }
+
+        public int ListDevices()
+        {
+            WriteCommand("L" + EOL);
+            return 0;
+        }
+
+        public int SetBlinkingCycle(int cycle)
+        {
+			WriteCommand("i" + Helper.IntToHex(cycle) + EOL);
+			return 0;
+        }
+
+        public int SetBlinkingInterval(int interval)
+        {
+			WriteCommand("I" + Helper.IntToHex(interval) + EOL);
+			return 0;
+        }
+
+        public int SetChannels(int[] cid, int[] pwm)
+        {
+            if (cid.Length > 0 && cid.Length == pwm.Length)
+            {
+                String channels = Helper.IntArrayToNumHexString(cid);
+                String pwms = Helper.IntArrayToNumHexString(pwm, false);
+                WriteCommand("n" + channels + pwms + EOL);
+                return 0;
+            }
+            return -1;
+        }
+
+        public int SetChannels(int[] cid, int pwm)
+        {
+            if (cid.Length > 0)
+            {
+                String channels = Helper.IntArrayToNumHexString(cid);
+                WriteCommand("m" + channels + Helper.IntToHex(pwm) + EOL);
+                return 0;
+            }
+            return -1;
+        }
+
+        public void ToggleDebug()
+        {
+            WriteCommand("D" + EOL);
+        }
+
+        public int SetComPort(int comport)
+        {
+            if (comport > 0)
+                _com = comport;
+            else
+                return -1;
+            return _com;
+        }
+
+
+        private void WriteCommand(String command)
+        {
+            if (_io != null && _io.IsOpen)
+            {
+                Console.WriteLine("CMD: " + command);
+                _io.Write(command);
+            }
+            else
+            {
+                Console.WriteLine("Could not send command (IO is null or closed): " + command);
+            }
+        }
+
+    }
+}

+ 44 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/Helper.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ActuBoardAPI
+{
+    class Helper
+    {
+
+        public static byte[] IntToHexByte(int i)
+        {
+            byte[] intBytes = BitConverter.GetBytes(i);
+            if (BitConverter.IsLittleEndian)
+                Array.Reverse(intBytes);
+            return intBytes;
+        }
+
+        public static String IntToHex(int i)
+        {
+            return i.ToString("X2");
+        }
+
+
+        public static String IntArrayToNumHexString(int[] cids, bool leadingCnt)
+        {
+            String channels = "";
+            if (cids.Length > 0)
+            {
+                if(leadingCnt)
+                    channels += Helper.IntToHex(cids.Length);
+                foreach (int c in cids)
+                    channels += Helper.IntToHex(c);
+            }
+            return channels;
+        }
+
+        public static String IntArrayToNumHexString(int[] cids)
+        {
+            return IntArrayToNumHexString(cids, true);
+        }
+
+    }
+}

+ 38 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/IActuBoard.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ActuBoardAPI
+{
+    public interface IActuBoard
+    {
+
+        int Connect();
+        int Disconnect();
+        bool IsConnected();
+        int SetComPort(int comport);
+
+        int SetChannel(int cid, int pwm);
+        int SetChannel(string cid, string pwm);
+        int GetChannel(int cid);
+
+        /// <summary> 
+        /// Set multiple channels at once to a given pwm value.
+        /// </summary> 
+        /// <param name="cid"></param> 
+        int SetChannels(int[] cid, int pwm);
+        int SetChannels(int[] cid, int[] pwm);
+        int SetBlinkingInterval(int interval);
+        int SetBlinkingCycle(int cycle);
+        int EnableBlinking(int cid);
+        int DisableBlinking(int cid);
+        int EnableBlinking(int[] cid);
+        int DisableBlinking(int[] cid);
+        void EnableOutput(bool enable);
+        int ListDevices();
+        void ToggleDebug();
+        void SendCustomCommand(string cmd);
+    }
+
+}

+ 36 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ActuBoardInterface")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ActuBoardInterface")]
+[assembly: AssemblyCopyright("Copyright ©  2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("22cdaa71-52c6-486e-b6ae-f459a47350a1")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 17 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/exceptions/ActuBoardException.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ActuBoardAPI
+{
+    class ActuBoardException : Exception
+    {
+
+        public ActuBoardException()
+        {
+
+        }
+
+    }
+}

+ 19 - 0
ActuBoard_API/ActuBoardInterface/ActuBoardInterface/exceptions/StatusCode.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ActuBoardAPI
+{
+    public enum StatusCode
+    {
+        NoError,
+        HardwareCommandError,
+        ChannelInvalid,
+        PwmValueInvalid,
+        ActuBoardNotConnected,
+        ActuBoardConnected,
+        ActuBoardIsConnecting,
+        ComPortInvalid
+    }
+}

+ 97 - 0
ActuBoard_Firmware/ActuBoard/ActuBoard.ino

@@ -0,0 +1,97 @@
+#include <dummy.h>
+
+#include "doubleSerial.h"
+#include "cmdInterpreter.h"
+#include "pca9635.h"
+
+#define LED_PIN				2
+#define OE_PIN				27
+#define MAX_PWM_BOARDS		12	// limited to the fact that not more i2c addresses are available with HW rev A1
+
+doubleSerial doubleS;
+cmdInterpreter cmdI;
+pca9635 pwm_driver[MAX_PWM_BOARDS];
+
+/**
+ * read from selected Serial if available
+ * write it to cmdInterpreter and return if end of command is detected
+ */
+boolean fillBuffer(void) {
+	uint8_t d = 0;
+	boolean r = doubleS.readSerial(&d);
+	
+	//something got read?
+	if (r) {
+		return cmdI.addToBuffer(d);
+	}
+	return false;
+}
+
+
+
+void setOE(boolean enable) {
+	digitalWrite(OE_PIN, enable);
+}
+
+/**
+ * setup task
+ */
+void setup() {
+	doubleS.selectSerial(false);
+	cmdI.prepareForNextCommand();
+
+	pinMode(LED_PIN, OUTPUT);
+	digitalWrite(LED_PIN, 0);
+
+	pinMode(OE_PIN, OUTPUT);
+	digitalWrite(OE_PIN, HIGH);
+
+	for (uint8_t i = 0; i < MAX_PWM_BOARDS; i++) {
+		if (!i) {
+			// init i2c on the first device
+			pwm_driver[i].begin(0x04, true, true);
+		} else {
+			// skip i2c init on others
+			pwm_driver[i].begin(0x04+i, false, true);
+		}
+		pwm_driver[i].set_led_mode(2);
+	}
+}
+
+
+/**
+ * task
+ */
+void loop() {
+	static uint8_t ButtonlastPinState = 1;
+	static uint16_t ledBlink = 0;
+	uint8_t pinState = digitalRead(0);
+
+	//button handling
+	if(!pinState && ButtonlastPinState){
+		boolean useBT = !doubleS.isBT(); 
+		digitalWrite(LED_PIN, useBT);
+		doubleS.selectSerial(useBT);
+		cmdI.prepareForNextCommand();
+	}
+	ButtonlastPinState = pinState;
+
+	//command handling
+	if (fillBuffer()) {
+		//cmd end received, lets analyse the command
+		uint8_t r = cmdI.interprete(pwm_driver, MAX_PWM_BOARDS, OE_PIN);
+		cmdI.prepareForNextCommand();
+		doubleS.writeSerial(r);
+	}
+
+	// let the led blink, if a user is connected to bluetooth serial console 
+	if (doubleS.isBT()) {
+		if (ledBlink++ == 0) {
+			if (doubleS.hasClient()) {
+				digitalWrite(LED_PIN, !digitalRead(LED_PIN));
+			} else {
+				digitalWrite(LED_PIN, true);
+			}
+		} 
+	}
+}

+ 625 - 0
ActuBoard_Firmware/ActuBoard/cmdInterpreter.cpp

@@ -0,0 +1,625 @@
+#include "cmdInterpreter.h"
+
+#define CMD_M_MAX_LENGTH	121		// 120 channels + 1 pwm value
+#define CMD_N_MAX_LENGTH	240		// 120 channels + 120 pwm values
+
+
+// Constructor
+cmdInterpreter::cmdInterpreter() {
+	this->prepareForNextCommand();
+	this->debug = false;
+}
+
+
+/**
+ * prepare everything for the next command
+ */
+void cmdInterpreter::prepareForNextCommand(void) {
+	memset((void *) this->cmdBuffer, 0x00, CMD_BUFFER_LENGTH);
+	this->cmdBuffer_ptr = this->cmdBuffer;
+}
+
+
+
+/**
+ * analyse command
+ */
+return_code_t cmdInterpreter::interprete(pca9635 pwm_driver[], uint8_t cnt, uint8_t oe_pin) {
+	uint8_t cmd_len = (uint8_t) strlen((char *) this->cmdBuffer);
+	uint8_t *cmd_buf_pntr = this->cmdBuffer;
+	cmd_buf_pntr++;        // skip command identifier
+	return_code_t returncode = RETURN_ERROR;
+
+	// check if all chars are valid hex chars
+	while (*cmd_buf_pntr) {
+		if(*cmd_buf_pntr == CMD_CR){
+			*cmd_buf_pntr = 68;
+		}
+
+		if (!isxdigit(*cmd_buf_pntr)) {
+			return RETURN_ERROR;
+		}
+		++cmd_buf_pntr;
+	}
+	cmd_buf_pntr = this->cmdBuffer;    // reset pointer
+
+	switch(*cmd_buf_pntr) {
+		// set single channel PWM
+		case 'S': {
+			uint8_t data[2] = {0}; //parameter 0: channel, parameter 1: pwm
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 2);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (data[0] >= 120) {
+				return RETURN_ERROR;
+			}
+
+			char s[64] = {'\0'};
+			if (this->debug) {
+				sprintf(s, " set channel %d to %d", data[0], data[1]);
+				doubleS.writeSerial(s);
+			}
+		
+			uint8_t dev = data[0] / 10;
+			uint8_t port = data[0] % 10;
+
+			if (this->debug) {
+				sprintf(s, " (dev: %d ch %d)", dev, port);
+				doubleS.writeSerial(s);
+			}
+			if (dev < cnt && pwm_driver[dev].isAvailable()) {
+				if (pwm_driver[dev].set_led_pwm(port, data[1])) {
+					if (this->debug)
+						doubleS.writeSerial(" channel is set!");
+				} else {
+					if (this->debug)
+						doubleS.writeSerial(" error while setting channel!");
+					return RETURN_ERROR;
+				}
+			} else {
+				if (this->debug)
+					doubleS.writeSerial(" dev not available!");
+				return RETURN_ERROR;
+			}
+			break;
+		}
+
+
+		// read single channel PWM
+		case 'R': {
+			uint8_t data[1] = {0}; //parameter 0: channel
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 1);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (data[0] >= 120) {
+				return RETURN_ERROR;
+			}
+
+			uint8_t dev = data[0] / 10;
+			uint8_t port = data[0] % 10;
+
+			
+		
+			if (dev < cnt && pwm_driver[dev].isAvailable()) {
+				uint8_t pwm = pwm_driver[dev].getPWMValue(port);
+				
+				char s[64] = {'\0'};
+				sprintf(s, " pwm value of channel %d is %d (hex: %X)", data[0],pwm, pwm);
+				doubleS.writeSerial(s);
+				
+			}
+			break;
+		}
+		
+		// set the output enable (1 means enable, 0 means disalble outputs)
+		case 'O': {
+			uint8_t data[1] = {0}; //parameter 0: value
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 1);
+			
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			digitalWrite(oe_pin, !data[0]);
+
+			break;
+		}
+
+		// set the blinking interval
+		case 'I': {
+			uint8_t data[1] = {0}; //parameter 0: value
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 1);
+			
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+			
+			char s[64] = {'\0'};
+			if (this->debug) {
+				sprintf(s, " set group interval to : %d (%dms)", data[0], (416*(data[0]+1))/10);
+				doubleS.writeSerial(s);
+			}
+			for (uint8_t i = 0; i < cnt; i++) {
+				if (pwm_driver[i].isAvailable()) {
+					if (pwm_driver[i].set_group_blinking(data[0])) {
+						if (this->debug)
+							doubleS.writeSerial(" set!");
+					} else {
+						if (this->debug)
+							doubleS.writeSerial(" error while setting!");
+						return RETURN_ERROR;
+					}
+				}
+			}
+			
+			
+			break;
+		}
+
+		// set the blinking dutycycle
+		case 'i': {
+			uint8_t data[1] = {0}; //parameter 0: value
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 1);
+			
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+			
+			char s[64] = {'\0'};
+			if (this->debug) {
+				sprintf(s, " set group duty to : %d", data[0]);
+				doubleS.writeSerial(s);
+			}
+			for (uint8_t i = 0; i < cnt; i++) {
+				if (pwm_driver[i].isAvailable()) {
+					if (pwm_driver[i].set_group_dimming(data[0])) {
+						if (this->debug)
+							doubleS.writeSerial(" set!");
+					} else {
+						if (this->debug)
+							doubleS.writeSerial(" error while setting!");
+						return RETURN_ERROR;
+					}
+				}
+			}
+			
+			
+			break;
+		}
+
+		// enable/disable the group blinking for a given channel
+		case 'G': {
+			uint8_t data[2] = {0}; //parameter 0: channel, parameter 1: bool
+			returncode = readParameters(cmd_buf_pntr, cmd_len, data, 2);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (data[0] >= 120) {
+				return RETURN_ERROR;
+			}
+
+			uint8_t dev = data[0] / 10;
+			uint8_t port = data[0] % 10;
+
+			if (data[1] > 1) {
+				data[1] = 1;
+			}
+			
+			char s[64] = {'\0'};
+			if (this->debug) {
+				sprintf(s, " set group blinking of channel %d (dev: %d port: %d) to %d", data[0], dev, port, data[1]);
+				doubleS.writeSerial(s);
+			}
+			if (pwm_driver[dev].isAvailable()) {
+				if (pwm_driver[dev].set_led_mode(port, data[1]+2)) {
+					if (this->debug)
+						doubleS.writeSerial(" set!");
+				} else {
+					if (this->debug)
+						doubleS.writeSerial(" error while setting!");
+					return RETURN_ERROR;
+				}
+			} else {
+				if (this->debug)
+					doubleS.writeSerial(" dev not available!");
+				return RETURN_ERROR;
+			}
+			
+			
+			break;
+		}
+
+
+		// set pwm value to multiple pwm channels
+		case 'm': {
+			uint8_t data[CMD_M_MAX_LENGTH] = {0}; //parameter n-1: channels, parameter last: pwm
+			uint8_t length = CMD_M_MAX_LENGTH;
+			char tmp[50];
+			returncode = readParametersWithLength(cmd_buf_pntr, cmd_len, data, &length, MODE_SINGLE_VALUE);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (this->debug) {
+				doubleS.writeSerial(" Data: ");
+				PrintHex8(tmp, data, length);
+				doubleS.writeSerial(tmp);
+				sprintf(tmp, " array length: %d", length);
+				doubleS.writeSerial(tmp);
+			}
+
+			// sort all channels
+			boolean change_pwm[12*10] = {false};
+			uint8_t new_pwm[12*10] = {0};
+			for (uint8_t i = 0; i < data[0]; i++) {
+				uint8_t channel = data[i+1];
+				if (!pwm_driver[channel/10].isAvailable()) {
+					return RETURN_ERROR;
+				}
+				new_pwm[channel] = data[length-1];
+				change_pwm[channel] = true;
+			}
+
+			
+			for (uint8_t i = 0; i < cnt; i++) {
+				int8_t channel_start = -1;
+				int8_t channel_end = -1;
+				//find first and last change
+				for (uint8_t j = 0; j < 10; j++) {
+					if (change_pwm[i*10+j]) {
+						if (channel_start == -1) 
+							channel_start = j;
+						channel_end = j;
+					}
+				}
+
+				// if no change for current device jump to next device
+				if (channel_start == -1 && channel_end == -1) {
+					continue;
+				}
+				if (this->debug) {
+					sprintf(tmp, "\rdev: %d changes: ", i);
+					doubleS.writeSerial(tmp);
+					PrintHex8(tmp, &change_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+
+					sprintf(tmp, " start: %d end %d", channel_start, channel_end);
+					doubleS.writeSerial(tmp);
+					
+					doubleS.writeSerial("\r        values: ");
+					PrintHex8(tmp, &new_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+				}
+				// read current values from devices (drivers internal copy)
+				for (uint8_t j = channel_start; j <= channel_end; j++) {
+					if (!change_pwm[i*10+j]) {
+						new_pwm[i*10+j] = pwm_driver[i].getPWMValue(j);
+					}
+				}
+				if (this->debug) {
+					doubleS.writeSerial("\rfilled  values: ");
+					PrintHex8(tmp, &new_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+				}
+
+				//write block to current device
+				boolean ret = pwm_driver[i].set_all_led_pwm(channel_start, channel_end-channel_start+1, &new_pwm[i*10+channel_start]);
+				if (!ret) {
+					return RETURN_ERROR;
+				}
+			}
+			
+			break;
+		}
+
+
+		// set pwm values to pwm channels
+		case 'n': {
+			uint8_t data[CMD_N_MAX_LENGTH] = {0}; //parameters: n channels, n pwm values
+			uint8_t length = CMD_N_MAX_LENGTH;
+			char tmp[50];
+			returncode = readParametersWithLength(cmd_buf_pntr, cmd_len, data, &length, MODE_MULTIPLE_VALUE);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (this->debug) {
+				doubleS.writeSerial(" Data: ");
+				PrintHex8(tmp, data, length);
+				doubleS.writeSerial(tmp);
+				sprintf(tmp, " array length: %d", length);
+				doubleS.writeSerial(tmp);
+			}
+
+			// sort all channels
+			boolean change_pwm[12*10] = {false};
+			uint8_t new_pwm[12*10] = {0};
+			for (uint8_t i = 0; i < data[0]; i++) {
+				uint8_t channel = data[i+1];
+				if (!pwm_driver[channel/10].isAvailable()) {
+					return RETURN_ERROR;
+				}
+				new_pwm[channel] = data[i+1+data[0]];
+				change_pwm[channel] = true;
+			}
+
+			
+			for (uint8_t i = 0; i < cnt; i++) {
+				int8_t channel_start = -1;
+				int8_t channel_end = -1;
+				//find first and last change
+				for (uint8_t j = 0; j < 10; j++) {
+					if (change_pwm[i*10+j]) {
+						if (channel_start == -1) 
+							channel_start = j;
+						channel_end = j;
+					}
+				}
+
+				// if no change for current device jump to next device
+				if (channel_start == -1 && channel_end == -1) {
+					continue;
+				}
+				if (this->debug) {
+					sprintf(tmp, "\rdev: %d changes: ", i);
+					doubleS.writeSerial(tmp);
+					PrintHex8(tmp, &change_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+
+					sprintf(tmp, " start: %d end %d", channel_start, channel_end);
+					doubleS.writeSerial(tmp);
+					
+					doubleS.writeSerial("\r        values: ");
+					PrintHex8(tmp, &new_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+				}
+				// read current values from devices (drivers internal copy)
+				for (uint8_t j = channel_start; j <= channel_end; j++) {
+					if (!change_pwm[i*10+j]) {
+						new_pwm[i*10+j] = pwm_driver[i].getPWMValue(j);
+					}
+				}
+				if (this->debug) {
+					doubleS.writeSerial("\rfilled  values: ");
+					PrintHex8(tmp, &new_pwm[i*10], 10);
+					doubleS.writeSerial(tmp);
+				}
+
+				//write block to current device
+				boolean ret = pwm_driver[i].set_all_led_pwm(channel_start, channel_end-channel_start+1, &new_pwm[i*10+channel_start]);
+				if (!ret) {
+					return RETURN_ERROR;
+				}
+			}
+			
+			break;
+		}
+
+
+		// set group blinking for multiple channels
+		case 'g': {
+			uint8_t data[CMD_N_MAX_LENGTH] = {0}; //parameter: n channels, n boolean group blinking
+			uint8_t length = CMD_N_MAX_LENGTH;
+			char tmp[50];
+			returncode = readParametersWithLength(cmd_buf_pntr, cmd_len, data, &length, MODE_MULTIPLE_VALUE);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			if (this->debug) {
+				doubleS.writeSerial(" Data: ");
+				PrintHex8(tmp, data, length);
+				doubleS.writeSerial(tmp);
+				sprintf(tmp, " array length: %d", length);
+				doubleS.writeSerial(tmp);
+			}
+
+			// sort all channels
+			uint8_t new_mode[12][10] = {0};
+			uint8_t new_led[12][10] = {0};
+			uint8_t change_cnt[12] = {0};
+			for (uint8_t i = 0; i < data[0]; i++) {
+				uint8_t channel = data[i+1];
+				if (!pwm_driver[channel/10].isAvailable()) {
+					return RETURN_ERROR;
+				}
+				new_mode[channel/10][change_cnt[channel/10]] = data[i+1+data[0]] + 2;
+				new_led[channel/10][change_cnt[channel/10]] = channel%10;
+				change_cnt[channel/10]++;	
+			}
+
+			
+			for (uint8_t i = 0; i < cnt; i++) {
+				// if no change for current device jump to next device
+				if (change_cnt[i] == 0) {
+					continue;
+				}
+				if (this->debug) {
+					sprintf(tmp, "\rdev: %d leds: ", i);
+					doubleS.writeSerial(tmp);
+					PrintHex8(tmp, new_led[i], 10);
+					doubleS.writeSerial(tmp);
+
+					sprintf(tmp, " cnt: %d\r     values: ", change_cnt[i]);
+					doubleS.writeSerial(tmp);
+					PrintHex8(tmp, new_mode[i], 10);
+					doubleS.writeSerial(tmp);
+				}
+				
+				//write block to current device
+				boolean ret = pwm_driver[i].set_all_led_modes(change_cnt[i], new_led[i], new_mode[i]);
+				if (!ret) {
+					return RETURN_ERROR;
+				}
+			}
+			
+			break;
+		}
+
+
+		// dump all available devices
+		case 'L': {
+			returncode = readParameters(cmd_buf_pntr, cmd_len, NULL, 0);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+			
+			char s[64] = {'\0'};
+
+			for (uint8_t i = 0; i < cnt; i++) {
+				if (pwm_driver[i].isAvailable()) {
+					sprintf(s, "dev %d available (channel: %d to: %d hex: %.2X to %.2X)\r\n", i, i*10, i*10+9, i*10, i*10+9);
+					doubleS.writeSerial(s);
+				}
+			}
+			break;
+		}
+		
+
+		// toogle debug messages
+		case 'D': {
+			returncode = readParameters(cmd_buf_pntr, cmd_len, NULL, 0);
+
+			if (returncode != RETURN_CR) {
+				return returncode;
+			}
+
+			this->debug = !this->debug;
+			break;
+		}
+		
+		
+		default: {
+			// end with error on unknown commands
+			return RETURN_ERROR;
+		}	
+	}
+	return RETURN_CR;
+}
+
+
+return_code_t cmdInterpreter::readParameters(uint8_t *cmd_buf_pntr, uint8_t cmd_len, uint8_t parameters[], uint8_t num_parameters) {
+	if (cmd_len != (2+2*num_parameters)) {
+		return RETURN_ERROR;
+	}
+
+	for (uint8_t i = 0; i < num_parameters; i++) {
+		parameters[i] = 0;
+		parameters[i] = ascii2byte(++cmd_buf_pntr) << 4;
+		parameters[i] += ascii2byte(++cmd_buf_pntr);
+		
+	}
+	return RETURN_CR;
+}
+
+/**
+ * this commands reads from a array and checks if the first hex block is a length that matches the rest of the data
+ * all parameters from string are converted to byte and stored in paramters array.
+ * the num_parameter pointer reads the maximal length of the parameters array and contains the written paraeters to parameters after the method.
+ * m controls the meode this method calculated the amount of expected parameters:
+ * - MODE_SINGLE_VALUE means there is an length field, length*bytes and a value field
+ * - MODE_MULTUPLE_VALUE means there is an length field, length*bytes and again length*value fields
+ */
+return_code_t cmdInterpreter::readParametersWithLength(uint8_t *cmd_buf_pntr, uint8_t cmd_len, uint8_t parameters[], uint8_t *num_parameters, cmdInterpreter::interpreteMode_t m) {
+	//check if length is available
+	if (cmd_len < (2+2)) {
+		return RETURN_ERROR;
+	}
+	parameters[0] = 0;
+	parameters[0] = ascii2byte(++cmd_buf_pntr) << 4;
+	parameters[0] += ascii2byte(++cmd_buf_pntr);
+
+	//check if length and command has the same length
+	uint8_t dataLength = 0;
+	switch(m) {
+		case MODE_SINGLE_VALUE:
+			dataLength = 2*parameters[0] + 2;
+			break;
+		case MODE_MULTIPLE_VALUE:
+			dataLength = 2*parameters[0] + 2*parameters[0];
+			break;
+		default:
+			break;
+	}
+	
+	if (cmd_len != (2+2+dataLength) || (dataLength/2+1) > *num_parameters) {
+		return RETURN_ERROR;
+	}
+
+	//interprete parameters from string
+	for (uint8_t i = 1; i < dataLength+1; i++) {
+		parameters[i] = 0;
+		parameters[i] = ascii2byte(++cmd_buf_pntr) << 4;
+		parameters[i] += ascii2byte(++cmd_buf_pntr);
+	}
+	*num_parameters = dataLength/2+1;
+	return RETURN_CR;
+}
+
+
+/**
+ * add to buffer and check if end of command is detected
+ * will return true if a end of command is detected
+ */
+boolean cmdInterpreter::addToBuffer(uint8_t d) {
+	*this->cmdBuffer_ptr = d;
+	if (d == CMD_CR) {	//check if the last character is the end character
+		if (this->debug) {
+			doubleS.writeSerial("Buffer: ");
+			char tmp[50];
+			PrintHex8(tmp, this->cmdBuffer, 20);
+			doubleS.writeSerial(tmp);
+		}
+		return true;
+	} else {	//if not, move pointer forward to next array address
+		(this->cmdBuffer_ptr)++;
+	}
+	return false;
+}
+
+
+/**
+ * prints 8-bit data in hex with leading zeroes
+ */
+void PrintHex8(char *dstptr, uint8_t *srcdata, uint8_t length) {
+	for (int i=0; i<length; i++) {
+		sprintf(dstptr+(i*2), "%.2X",srcdata[i]);
+	}
+}
+
+/**
+ * prints boolean data
+ */
+void PrintHex8(char *dstptr, boolean *srcdata, uint8_t length) {
+	for (int i=0; i<length; i++) {
+		sprintf(dstptr+(i*2), " %.1X",srcdata[i]);
+	}
+}
+
+
+/**
+ * takes a pointer to a string and convert the current characters to a binary
+ */
+uint8_t ascii2byte(const uint8_t *val) {
+	uint8_t temp = *val;
+
+	if (temp > 0x60)
+		temp -= 0x27;        // convert chars a-f
+	else if (temp > 0x40)
+		temp -= 0x07;        // convert chars A-F
+	temp -= 0x30;        // convert chars 0-9
+
+	return (uint8_t) (temp & 0x0F);
+}

+ 47 - 0
ActuBoard_Firmware/ActuBoard/cmdInterpreter.h

@@ -0,0 +1,47 @@
+// safety againts double-include
+#ifndef cmdInterpreter_h
+#define cmdInterpreter_h
+#include <Arduino.h>
+#include "doubleSerial.h"
+#include "pca9635.h"
+
+
+#define CMD_BUFFER_LENGTH	256 
+#define CMD_CR				'\r'
+
+typedef enum {
+	RETURN_ERROR	= 7,
+	RETURN_CR		= CMD_CR,
+} return_code_t;
+
+// Stub extension for now
+class cmdInterpreter {
+    public:
+        cmdInterpreter();
+
+		boolean addToBuffer(uint8_t d);
+		return_code_t interprete(pca9635 pwm_driver[], uint8_t cnt, uint8_t oe_pin);
+		void prepareForNextCommand();
+    private:
+    	uint8_t cmdBuffer[CMD_BUFFER_LENGTH];
+    	uint8_t *cmdBuffer_ptr = cmdBuffer;
+    	uint8_t debug = false;
+
+		typedef enum {
+			MODE_SINGLE_VALUE = 1,
+			MODE_MULTIPLE_VALUE = 2,
+		} interpreteMode_t;
+
+    	return_code_t readParameters(uint8_t *cmd_buf_pntr, uint8_t cmd_len, uint8_t parameters[], uint8_t num_parameters);
+    	return_code_t readParametersWithLength(uint8_t *cmd_buf_pntr, uint8_t cmd_len, uint8_t parameters[], uint8_t *num_parameters, cmdInterpreter::interpreteMode_t m);
+    	
+};
+
+extern doubleSerial doubleS;
+
+void PrintHex8(char *dstptr, uint8_t *srcdata, uint8_t length);
+void PrintHex8(char *dstptr, boolean *srcdata, uint8_t length);
+uint8_t ascii2byte(const uint8_t *val);
+
+#endif
+// *********** END OF CODE **********

+ 95 - 0
ActuBoard_Firmware/ActuBoard/doubleSerial.cpp

@@ -0,0 +1,95 @@
+#include "doubleSerial.h"
+
+// Constructor
+doubleSerial::doubleSerial() {
+	this->useBT = false;
+	this->BTName = "ActuBoard BLE";
+}
+
+doubleSerial::doubleSerial(const char *BT_Name) {
+	this->useBT = false;
+	this->BTName = BT_Name;
+}
+
+
+
+/**
+ * select which serial is used to communicate
+ *
+ */
+void doubleSerial::selectSerial(boolean useBluetooth) {
+	if (useBluetooth) {
+		Serial.end();
+		SerialBT.begin(this->BTName); //Bluetooth device name
+		SerialBT.flush();
+	} else {
+		SerialBT.end();
+		Serial.begin(115200, SERIAL_8N1, -1, -1, false, 1100UL);
+		Serial.flush();
+	}
+	this->useBT = useBluetooth;
+}
+
+
+/**
+ * read from selected Serial if available
+ * will return true if a character was read
+ */
+boolean doubleSerial::readSerial(uint8_t *data) {
+	//check if we have something to read
+	if (this->useBT) {
+		if (SerialBT.available()){
+			*data = SerialBT.read();
+			return true;
+		}
+	} else {
+		if (Serial.available()){
+			*data = Serial.read();
+			return true;
+		}
+	}
+	return false;
+}
+
+
+/**
+ * write one char to the selected Serial
+ */
+void doubleSerial::writeSerial(const char c) {
+	 if (this->useBT) {
+		SerialBT.write(c);
+	} else {
+		Serial.write(c);
+	}
+}
+
+
+/**
+ * write string char to the selected Serial
+ */
+void doubleSerial::writeSerial(const char *c) {
+	while(*c != 0) {
+		this->writeSerial(*c);
+		c++;
+	}
+}
+
+
+/**
+ * return of there is a client on the other end of the serial console
+ * in case of uart serial it is always true, bluetooth depends.
+ */
+boolean doubleSerial::hasClient(void) {
+	if (this->useBT) {
+		return SerialBT.hasClient();
+	} else {
+		return true;
+	}
+}
+
+/**
+ * return true if currently bluetooth is used for communication
+ */
+boolean doubleSerial::isBT(void) {
+	return this->useBT;
+}

+ 33 - 0
ActuBoard_Firmware/ActuBoard/doubleSerial.h

@@ -0,0 +1,33 @@
+// safety againts double-include
+#ifndef doubleSerial_h
+#define doubleSerial_h
+#include <Arduino.h>
+#include <HardwareSerial.h>
+#include "BluetoothSerial.h" 
+
+#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
+#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
+#endif
+
+// Stub extension for now
+class doubleSerial {
+    public:
+        doubleSerial();
+        doubleSerial(const char *BT_Name);
+		void selectSerial(boolean useBluetooth);
+		boolean isBT(void);
+		
+		boolean readSerial(uint8_t *data);
+		void writeSerial(const char c);
+		void writeSerial(const char *c);
+		boolean hasClient(void);
+    private:
+    	boolean useBT;
+    	const char *BTName = NULL;  
+    	BluetoothSerial SerialBT;
+    	
+};
+extern HardwareSerial Serial;
+
+#endif
+// *********** END OF CODE **********

+ 382 - 0
ActuBoard_Firmware/ActuBoard/pca9635.cpp

@@ -0,0 +1,382 @@
+//Source: https://github.com/rambo/pca9635
+
+#include "pca9635.h"
+
+// Constructor
+pca9635::pca9635() {
+    this->device_address = 0x70; // Default to the all-call address
+    this->autoincrement_bits = 0x80; // Autoincrement all
+    this->available = false;
+    for (uint8_t i = 0; i < NUM_CHANNELS; i++) {
+    	this->pwm_values[i] = 0;
+    }
+	for (uint8_t i = 0; i < MODE_REGISTERS; i++) {
+    	this->led_mode_regs[i] = 0;
+    }
+}
+
+void pca9635::begin(byte dev_addr, boolean wire_begin, boolean init)
+{
+	device_address = dev_addr;
+	if (wire_begin) {
+		if (!Wire.begin(21, 22, 400000UL)) {
+			Serial.println("Error while setup i2c bus!");
+		}
+		this->reset();
+	}
+    if (init)
+    {
+        if (this->set_sleep(0x0)) { // Wake up oscillators
+        	this->available = true;
+        	delayMicroseconds(500); // Wait for the oscillators to stabilize
+        	this->set_led_mode(0); // Default to full PWM mode for all drivers
+        	this->set_invert_output(1); //invert output channels to use with external nfet
+        	this->set_output_not_active_state(0x00); //LEDn=0 if OE is not set
+        	this->set_group_control(1); // setup group blinking
+        	this->set_group_dimming(0x80); // set duty cycle to 50%
+//			this->set_driver_mode(0x00); // set to open-drain
+        } else {
+        	this->available = false;
+        }
+    }
+}
+
+void pca9635::begin(byte dev_addr, boolean wire_begin)
+{
+    pca9635::begin(dev_addr, wire_begin, false);
+}
+
+boolean pca9635::readSingleReg(uint8_t reg, uint8_t *data) {
+
+    Wire.beginTransmission(this->device_address);
+    if (Wire.write(reg | autoincrement_bits) != 1) return false;
+    if (Wire.endTransmission(false)) return false; //send restart
+    if (Wire.requestFrom((uint16_t) this->device_address,1U,true) == 1U) { // read one byte, then send stop
+    	*data = Wire.read();
+    	return true;
+    }
+	return false;
+}
+
+boolean pca9635::writeSingleReg(uint8_t reg, uint8_t data) {
+    return this->writeManyReg(reg, 1, &data);
+    
+/*    Wire.beginTransmission(this->device_address);
+    if (Wire.write(reg | autoincrement_bits) != 1) return false;
+    if (Wire.write(data) != 1) return false;
+    if (!Wire.endTransmission(true)) { //send stop
+    	return true;
+    }
+	return false;
+	*/
+}
+
+boolean pca9635::writeManyReg(uint8_t reg, uint8_t cnt, uint8_t *data) {
+    Wire.beginTransmission(this->device_address);
+    if (Wire.write(reg | autoincrement_bits) != 1) return false;
+    if (Wire.write(data, cnt) != cnt) return false;
+    if (!Wire.endTransmission(true)) { //send stop
+    	return true;
+    }
+	return false;
+}
+
+
+void pca9635::exchangeValueReg(uint8_t *reg, uint8_t mask, uint8_t value) {
+	*reg = (*reg & mask) | value;
+}
+
+boolean pca9635::readModifyWriteReg(uint8_t reg, uint8_t mask, uint8_t value) {
+	uint8_t tmp;
+    if (!this->readSingleReg(reg, &tmp)) {
+    	Serial.println("error while reading single reg");
+    	return false;
+    }
+    this->exchangeValueReg(&tmp, mask, value);
+    return this->writeSingleReg(reg, tmp);
+}
+
+/**
+ * Control the led channel mode, modes: 
+ * 0=fully off
+ * 1=fully on (no PWM)
+ * 2=individual PWM only
+ * 3=individual and group PWM
+ *
+ * Remember that led numbers start from 0
+ */
+boolean pca9635::set_led_mode(byte ledno, byte mode)
+{
+    byte reg = 0x17;
+    if ( ledno >= 8 && ledno < 11)
+    {
+        reg = 0x16;
+    }
+    if ( ledno >= 4 && ledno < 8)
+    {
+        reg = 0x15;
+    }
+    if (ledno < 4)
+    {
+        reg = 0x14;
+    }
+    byte value = 0;
+    switch (mode)
+    {
+        case 0:
+            value = B00000000;
+            break;
+        case 1:
+            value = B01010101;
+            break;
+        case 2:
+            value = B10101010;
+            break;
+        case 3:
+            value = B11111111;
+            break;
+    }
+    byte mask = B00000000;
+    switch(ledno%4)
+    {
+        case 0:
+            mask = (byte)~B00000011;
+            break;
+        case 1:
+            mask = (byte)~B00001100;
+            break;
+        case 2:
+            mask = (byte)~B00110000;
+            break;
+        case 3:
+            mask = (byte)~B11000000;
+            break;
+	} 
+    this->exchangeValueReg(&this->led_mode_regs[ledno/4], mask, value);
+    return this->readModifyWriteReg(reg | autoincrement_bits, mask, value); 
+}
+
+/**
+ * Set mode for all leds 
+ * 0=fully off
+ * 1=fully on (no PWM)
+ * 2=individual PWM only
+ * 3=individual and group PWM
+ */
+boolean pca9635::set_led_mode(byte mode)
+{
+    byte value;
+    switch (mode)
+    {
+        case 0:
+            value = B00000000;
+            break;
+        case 1:
+            value = B01010101;
+            break;
+        case 2:
+            value = B10101010;
+            break;
+        case 3:
+            value = B11111111;
+            break;
+        default:
+            value = 0;
+            break;
+    }
+    for (uint8_t i = 0; i < MODE_REGISTERS; i++) {
+    	this->led_mode_regs[i] = value;
+    }
+    return this->writeManyReg(0x14 | autoincrement_bits, 4, this->led_mode_regs);
+}
+
+/**
+ * Set modes to given leds
+ * leds specified in the 'leds'-array
+ * modes specified in the 'modes'-array
+ */
+boolean pca9635::set_all_led_modes(uint8_t cnt, uint8_t leds[], uint8_t modes[]) {
+	for (uint8_t i = 0; i < cnt; i++) {
+		uint8_t mode = modes[i];
+		if (mode > 3) {
+			return false;
+		}
+		uint8_t ledno = leds[i];
+		if (ledno >= NUM_CHANNELS) {
+			return false;
+		}
+		this->exchangeValueReg(&this->led_mode_regs[ledno/4], ~(0x3<<(2*(ledno%4))), mode<<(2*(ledno%4)));
+	}
+	return this->writeManyReg(0x14 | autoincrement_bits, 4, this->led_mode_regs);
+}
+
+/**
+ * Enable given SUBADDRess (1-3)
+ */
+boolean pca9635::enable_subaddr(byte addr)
+{
+    byte value;
+    switch (addr)
+    {
+        case 1:
+            value = _BV(3); // 0x71
+            break;
+        case 2:
+            value = _BV(2); // 0x72
+            break;
+        case 3:
+            value = _BV(1); // 0x74
+            break;
+        default:
+            value = 0;
+            break;
+    }
+    byte mask = ~value;
+    return this->readModifyWriteReg(0x0 | autoincrement_bits, mask, value);
+}
+
+/**
+ * Changes the driver mode between open drain(0x0) and totem-pole (0x1, the default)
+ */
+boolean pca9635::set_driver_mode(byte mode)
+{
+    return this->readModifyWriteReg(0x01 | autoincrement_bits, (byte)~_BV(2), (mode & 0x01) << 2);
+}
+
+
+/**
+ * Changes the driver mode between not inverted(0x0, the default) and inverted (0x1)
+ */
+boolean pca9635::set_invert_output(byte mode)
+{
+    return this->readModifyWriteReg(0x01 | autoincrement_bits, (byte)~_BV(4), (mode & 0x01) << 4);
+}
+
+
+/**
+ * Changes the driver inactive state: LEDn = 0 (0x00), LEDn = 1 if driver_mode = 1 or LEDn = high-impedance if driver_mode = 0 (0x01, the default) or LEDn = high-impedance (0x10)
+ */
+boolean pca9635::set_output_not_active_state(byte mode)
+{
+	if (mode > 2) {
+		return false;
+	}
+    return this->readModifyWriteReg(0x01 | autoincrement_bits, (byte)~(_BV(0) | _BV(1)), (mode & 0x03));
+}
+
+
+/**
+ * Changes the group control mode between dimming(0x0, the default) and blinking (0x1)
+ */
+boolean pca9635::set_group_control(byte mode)
+{
+	
+    return this->readModifyWriteReg(0x01 | autoincrement_bits, (byte)~_BV(5), (mode & 0x01) << 5);
+}
+
+/**
+ * Changes the oscillator mode between sleep (0x1, the default) and active (0x0)
+ */
+boolean pca9635::set_sleep(byte sleep)
+{
+    return this->readModifyWriteReg(0x00 | autoincrement_bits, (byte)~_BV(4), (sleep & 0x01) << 4);
+}
+
+
+/**
+ * Sets the pwm value for given led, note that it must have previously been enabled for PWM control with set_mode
+ * 
+ * Remember that led numbers start from 0
+ */
+boolean pca9635::set_led_pwm(byte ledno, byte cycle) {
+	byte reg = 0x02 + ledno;
+	boolean ret = this->writeManyReg(reg | autoincrement_bits, 1, &cycle);
+	if (ret) {
+		this->pwm_values[ledno] = cycle;
+	}
+	return ret;
+}
+
+
+/**
+ * Sets the pwm values for given leds, start led and the cnt of leds to change, note that it must have previously been enabled for PWM control with set_mode
+ * the value for each led has to be set in the array.
+ *
+ * Remember that led numbers start from 0
+ */
+boolean pca9635::set_all_led_pwm(uint8_t start_led, uint8_t cnt, uint8_t *cycles) {
+	if (start_led >= NUM_CHANNELS || (start_led+cnt) >= NUM_CHANNELS) {
+		return false;
+	}
+	boolean ret = this->writeManyReg((start_led + 0x02) | autoincrement_bits, cnt, cycles);
+	if (ret) {
+		for (uint8_t i = 0; i < cnt; i++) {
+			pwm_values[i+start_led] = *(cycles+i);
+		}
+	}
+	return ret;
+}
+
+/**
+ * Sets the Group pwm value, note that it must have previously been enabled for PWM control and group PWM with set_mode
+ */
+boolean pca9635::set_group_dimming(uint8_t cycle) {
+	return this->writeManyReg(0x12 | autoincrement_bits, 1, &cycle);
+}
+
+
+/**
+ * Sets the Group blinking frequency, note that it must have previously been enabled for PWM control and group PWM with set_mode
+ */
+boolean pca9635::set_group_blinking(uint8_t freq) {
+	return this->writeManyReg(0x13 | autoincrement_bits, 1, &freq);
+}
+
+
+/**
+ * Do the software-reset song-and-dance, this should reset all drivers on the bus
+ */
+boolean pca9635::reset(uint8_t addr) {
+#ifdef I2C_DEVICE_DEBUG
+    Serial.println(F("pca9635::reset() called"));
+#endif
+	Wire.beginTransmission(addr);
+	uint8_t byteArray[2] = {0xA5, 0x5A};
+	if (Wire.write(byteArray, 2) != 2) return false;
+
+    byte result = Wire.endTransmission(true);
+    if (result > 0)
+    {
+#ifdef I2C_DEVICE_DEBUG
+        Serial.print(F("FAILED: Wire.write(0x03, 0x5a, 0xa5); returned: "));
+        Serial.println(result, DEC);
+#endif
+        return false;
+    }
+    delayMicroseconds(5); // Wait for the reset to complete
+    return true;
+}
+
+
+boolean pca9635::reset() {
+	return this->reset(0x03);
+}
+
+
+boolean pca9635::isAvailable() {
+	return this->available;
+}
+
+/**
+ * return the current configured pwm value for the selected channel
+ */
+uint8_t pca9635::getPWMValue(uint8_t channel) {
+	if (channel < NUM_CHANNELS) {
+		return this->pwm_values[channel];
+	} else {
+		return 0;
+	}
+}
+
+// Instance for the all-call address
+pca9635 PCA9635 = pca9635();

+ 58 - 0
ActuBoard_Firmware/ActuBoard/pca9635.h

@@ -0,0 +1,58 @@
+//Source: https://github.com/rambo/pca9635
+
+
+// safety againts double-include
+#ifndef pca9635_h
+#define pca9635_h
+#include <Arduino.h> 
+#include <Wire.h>
+
+#define NUM_CHANNELS		16
+#define MODE_REGISTERS		4
+
+// Stub extension for now
+class pca9635 {
+    public:
+        pca9635(); // We need this so we can set default address to the all-call one for the PCA9635 instance
+		
+        void begin(byte dev_addr, boolean wire_begin);
+        void begin(byte dev_addr, boolean wire_begin, boolean init); // Variant to allow skipiing init (needed by the RGB board)
+        boolean set_led_mode(byte ledno, byte mode);
+        boolean set_led_mode(byte mode); // Variant to set all leds to same mode
+        boolean set_all_led_modes(uint8_t cnt, uint8_t leds[], uint8_t modes[]);
+        boolean set_led_pwm(byte ledno, byte cycle);
+        boolean set_all_led_pwm(uint8_t start_led, uint8_t cnt, uint8_t *cycles);
+        boolean set_group_dimming(uint8_t cycle); //190.7Hz (5.24ms) signal, duty = cycle * 2 * 256 * 40ns
+        boolean set_group_blinking(uint8_t freq); //freq * 41.6 ms
+        boolean set_driver_mode(byte mode);
+        boolean set_invert_output(byte mode);
+       	boolean set_output_not_active_state(byte mode);
+        boolean set_group_control(byte mode);
+        boolean set_sleep(byte sleep);
+        boolean enable_subaddr(byte addr);
+        boolean isAvailable();
+        uint8_t getPWMValue(uint8_t channel);
+        
+        boolean reset(); // NOTE: This resets all PCA9635 devices on the bus
+        
+    private:
+    	uint8_t device_address;
+    	uint8_t autoincrement_bits;
+    	boolean available;
+    	uint8_t pwm_values[NUM_CHANNELS];
+    	uint8_t led_mode_regs[MODE_REGISTERS];
+    	
+    	boolean readSingleReg(uint8_t reg, uint8_t *data);
+    	boolean writeSingleReg(uint8_t reg, uint8_t data);
+    	boolean writeManyReg(uint8_t reg, uint8_t cnt, uint8_t *data);
+    	void exchangeValueReg(uint8_t *reg, uint8_t mask, uint8_t value);
+    	boolean readModifyWriteReg(uint8_t reg, uint8_t mask, uint8_t value);
+    	boolean reset(uint8_t addr);
+    	
+};
+
+extern pca9635 PCA9635;
+
+
+#endif
+// *********** END OF CODE **********

+ 14 - 0
README.md

@@ -0,0 +1,14 @@
+Contains two projects:
+
+1) ActuBoard Firmware
+	C++ firmware for the ESP32 and the ActuBoard specific hardware (Controller + Hubs). Also provide the serial communication interface on hardware side, as well as the addressing of each Hub port through I2C.
+
+2) ActuBoard API
+	2.1) ActuBoard Interface
+		Unity compatible C# library for serial communication with ActuBoard. Can be imported easily as DLL in own projects.
+		
+	2.2) ActuBoard Console
+		Debugging tool that relies on the ActuBoard Interface. Good to test commands without having a project ready. Also, includes a terminal-like output which makes Putty or similar not necessary.
+		
+3) Schematics
+	Contains schematics, renderings, list of commands and everything necessary to rebuild ActuBoard.

BIN
Schematics/Commands_ActuBoard.xlsx