ln.ethercat/ln.ethercat/ECMaster.cs

572 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using ln.ethercat.controller;
using ln.ethercat.controller.drives;
using ln.logging;
using ln.type;
namespace ln.ethercat
{
public enum ECMasterState {
INITIALIZED,
STARTING,
STOPPING,
RUNNING,
}
public struct PDOMappingRequest
{
public ushort Slave;
public ushort Index;
public byte SubIndex;
public bool RxPDO;
public PDOMappingRequest(ushort slave,ushort index,byte subIndex,bool rxpdo)
{
Slave = slave;
Index = index;
SubIndex = subIndex;
RxPDO = rxpdo;
}
}
public delegate void ECStateChange(ECMaster sender,ECSlaveState newState);
public delegate void ProcessDataExchanged(ECMaster ecMaster);
public class ECMaster
{
public int TIMEOUT_PREOP = 3000;
public int TIMEOUT_SAFEOP = 10000;
public int TIMEOUT_BACKTO_SAFEOP = 200;
public int INTERVALL_PROCESSDATA = 1;
public int INTERVALL_WATCHDOG = 250;
public event ECStateChange OnStateChange;
public event ProcessDataExchanged OnProcessDataExchanged;
public string InterfaceName { get; }
public int ExpectedWorkCounter { get; private set; }
public int CountSlaves { get; private set; }
public IntPtr IOMapPtr { get; private set; }
public int IOMapSize { get; private set; }
public ECMasterState MasterState { get; private set; } = ECMasterState.INITIALIZED;
ECSlaveState ethercatState = ECSlaveState.NONE;
public ECSlaveState EthercatState {
get =>ethercatState;
private set {
if (value != ethercatState)
{
OnStateChange?.Invoke(this, value);
ethercatState = value;
Logging.Log(LogLevel.DEBUG, "ECMaster: EthercatState is now {0}", ethercatState);
}
}
}
List<PDO> pdoMap = new List<PDO>();
public PDO[] GetPDOMap() => pdoMap.ToArray();
Dictionary<UInt16, ECSlave> slaves = new Dictionary<ushort, ECSlave>();
public ECMaster(string interfaceName)
{
InterfaceName = interfaceName;
int result = ECMBind.ecmbind_initialize(interfaceName);
Logging.Log(LogLevel.INFO, "ecmbind_initialize({0}) = {1}", interfaceName, result);
if (result<=0)
throw new Exception("ecmbind_initialize failed");
Controller = new Controller(this);
}
bool stopWatchdog;
bool stopProcessData;
Thread threadProcessData;
Thread threadWatchdog;
public Controller Controller { get; }
List<DriveController> driveControllers = new List<DriveController>();
public DriveController[] DriveControllers => driveControllers.ToArray();
public void Start()
{
lock (this)
{
if (threadWatchdog?.IsAlive ?? false)
throw new Exception("already started");
ethercatState = ECSlaveState.NONE;
ExpectedWorkCounter = 0;
stopWatchdog = false;
threadWatchdog = new Thread(Watchdog);
threadWatchdog.Start();
}
}
public void Stop()
{
lock (this)
{
if (threadWatchdog?.IsAlive ?? false)
{
stopWatchdog = true;
lock (this)
Monitor.PulseAll(this);
threadWatchdog.Join();
}
}
}
void Watchdog()
{
while (!stopWatchdog)
{
IOMapSize = 0;
if (threadProcessData?.IsAlive ?? false)
{
stopProcessData = true;
while (threadProcessData.IsAlive)
threadProcessData.Join(250);
}
stopProcessData = false;
EthercatState = ECSlaveState.INIT;
CountSlaves = ECMBind.ecmbind_config_init();
if (CountSlaves <= 0)
{
Logging.Log(LogLevel.DEBUG, "ECMaster: no slaves connected, scheduling restart...");
Thread.Sleep(2500);
continue;
}
if (!WaitForState(ECSlaveState.PRE_OP, TIMEOUT_PREOP))
{
Logging.Log(LogLevel.DEBUG, "ECMaster: slaves did not reach PRE_OP. restart...");
Thread.Sleep(2500);
continue;
}
/* Bus State PRE_OP */
ScanControllers();
EthercatState = ECSlaveState.PRE_OP;
ConfigureProcessDataMappings();
SetupPDOMapping();
UpdatePDOMap();
threadProcessData = new Thread(ProcessData);
threadProcessData.Start();
Thread.Sleep(20);
RequestSlaveState(0, ECSlaveState.SAFE_OP);
if (!WaitForState(ECSlaveState.SAFE_OP, TIMEOUT_SAFEOP))
{
Logging.Log(LogLevel.DEBUG, "ECMaster: slaves did not reach SAFE_OP. restart...");
Thread.Sleep(2500);
continue;
}
EthercatState = ECSlaveState.SAFE_OP;
Thread.Sleep(20);
RequestSlaveState(0, ECSlaveState.OPERATIONAL);
Thread.Sleep(250);
if (!WaitForState(ECSlaveState.OPERATIONAL, TIMEOUT_SAFEOP))
{
Logging.Log(LogLevel.DEBUG, "ECMaster: slaves did not reach OPERATIONAL. restart...");
Thread.Sleep(2500);
continue;
}
EthercatState = ECSlaveState.OPERATIONAL;
/***** we handle the controller cycle from here on *****/
Controller.Initialize();
DateTime nextControllerRun = DateTime.Now;
while (!stopWatchdog)
{
EthercatState = IOLocked(()=>ECMBind.ecmbind_read_state());
if (EthercatState != ECSlaveState.OPERATIONAL)
break;
DateTime currentTime = DateTime.Now;
nextControllerRun += TimeSpan.FromSeconds(Controller.ControllerLoopInterval);
if (currentTime > nextControllerRun)
{
Logging.Log(LogLevel.WARNING, "missed controller cycle");
while (currentTime > nextControllerRun)
nextControllerRun += TimeSpan.FromSeconds(Controller.ControllerLoopInterval);
}
TimeSpan timeToNextCycle = nextControllerRun - currentTime;
try
{
Thread.Sleep(timeToNextCycle);
Controller.Cycle();
} catch (Exception e)
{
Logging.Log(e);
}
}
}
}
void ProcessData()
{
int wkc;
lock (lockIOMap)
ExpectedWorkCounter = ECMBind.ecmbind_get_expected_wkc_size();
Logging.Log(LogLevel.INFO, "ECMaster: ProcessData(): start");
while (!stopProcessData)
{
lock (lockIOMap)
{
wkc = ECMBind.ecmbind_processdata2(iomap, iomap.Length);
if (wkc != ExpectedWorkCounter)
{
int success = ECMBind.ecmbind_recover();
if (success > 0)
{
Logging.Log(LogLevel.ERROR, "ECMaster: bus recovery successfull...");
ExpectedWorkCounter = success;
//SetupPDOMapping();
} else
{
RequestSlaveState(0, ECSlaveState.INIT);
Logging.Log(LogLevel.ERROR, "ECMaster: bus recovery failed, scheduling restart...");
ScheduleRestart();
stopProcessData = true;
}
} else {
OnProcessDataExchanged?.Invoke(this);
}
}
Thread.Sleep(INTERVALL_PROCESSDATA);
}
Logging.Log(LogLevel.INFO, "ECMaster: ProcessData(): finished");
}
public void ScanControllers()
{
driveControllers.Clear();
for (UInt16 slave_id = 1; slave_id <= CountSlaves; slave_id++)
{
if (ReadSDO(slave_id, 0x1000, 0, out byte[] bDeviceType))
{
ushort profilecode = BitConverter.ToUInt16(bDeviceType);
Logging.Log(LogLevel.INFO, "ECMaster: ScanControllers: Slave {0} has DeviceType {1} [{2}]", slave_id, profilecode, bDeviceType.ToHexString());
switch (profilecode)
{
case 402:
driveControllers.Add(new CIA402Controller(this, slave_id));
break;
}
}
}
}
bool WaitForState(ECSlaveState expectedState, int timeout)
{
int waitIncrement = 100;
ECSlaveState currentState = IOLocked(()=>ECMBind.ecmbind_read_state());
while ((timeout > 0) && (currentState != expectedState))
{
Thread.Sleep(waitIncrement);
timeout -= waitIncrement;
currentState = IOLocked(()=>ECMBind.ecmbind_read_state());
}
return currentState == expectedState;
}
object lockIOMap = new object();
byte[] iomap = new byte[8192];
void SetupPDOMapping()
{
IOMapSize = ECMBind.ecmbind_config_map();
Logging.Log(LogLevel.DEBUG, "ECMaster: IOMapSize={0}", IOMapSize);
//UpdatePDOMap();
}
HashSet<PDOMappingRequest> mappingRequests = new HashSet<PDOMappingRequest>();
public void ConfigureProcessDataMappings()
{
Logging.Log(LogLevel.INFO, "configure PDO mappings");
for (UInt16 slave = 1; slave <= CountSlaves; slave++)
{
List<int> rxPDOs = new List<int>();
List<int> txPDOs = new List<int>();
foreach (PDOMappingRequest mappingRequest in mappingRequests)
{
if (mappingRequest.Slave == slave)
{
if (GetSDOValue(mappingRequest.Slave, mappingRequest.Index, mappingRequest.SubIndex, out SDOValue sdoValue))
{
int mappingEntry = (mappingRequest.Index << 16) | (mappingRequest.SubIndex << 8) | sdoValue.BitLength;
Logging.Log(LogLevel.DEBUG, "PDO mapping: {0:X8}", mappingEntry);
if (mappingRequest.RxPDO)
rxPDOs.Add(mappingEntry);
else
txPDOs.Add(mappingEntry);
} else {
Logging.Log(LogLevel.WARNING, "ECMaster: SetupProcessDataMappings(): could not retrieve SDOValue {0}:{1:X4}.{2}", mappingRequest.Slave, mappingRequest.Index, mappingRequest.SubIndex);
}
}
}
MemoryStream rxStream = new MemoryStream();
rxStream.WriteByte((byte)rxPDOs.Count);
rxStream.WriteByte(0);
foreach (int pdoMapping in rxPDOs)
rxStream.WriteBytes(pdoMapping.GetBytes());
Logging.Log(LogLevel.DEBUG, "RxPDO mappings for slave {0} => {1}", slave, rxPDOs.Count);
if (!WriteSDOCA(slave, 0x1600, 0, rxStream.ToArray()))
{
Logging.Log(LogLevel.WARNING, "ECMaster: SetupProcessDataMappings(): could not write RxPDO mappings for slave {0}", slave);
}
MemoryStream txStream = new MemoryStream();
txStream.WriteByte((byte)txPDOs.Count);
txStream.WriteByte(0);
foreach (int pdoMapping in txPDOs)
txStream.WriteBytes(pdoMapping.GetBytes());
Logging.Log(LogLevel.DEBUG, "TxPDO mappings for slave {0} => {1}", slave, txPDOs.Count);
if (!WriteSDOCA(slave, 0x1A00, 0, txStream.ToArray()))
{
Logging.Log(LogLevel.WARNING, "ECMaster: SetupProcessDataMappings(): could not write TxPDO mappings for slave {0}", slave);
}
if (!WriteSDOCA(slave, 0x1c12, 0, new byte[]{ 0x01, 0x00, 0x00, 0x16 }))
{
Logging.Log(LogLevel.WARNING, "ECMaster: SetupProcessDataMappings(): could not configure syncmanager 2 PDO assignment for slave {0}", slave);
}
if (!WriteSDOCA(slave, 0x1c13, 0, new byte[]{ 0x01, 0x00, 0x00, 0x1A }))
{
Logging.Log(LogLevel.WARNING, "ECMaster: SetupProcessDataMappings(): could not configure syncmanager 3 PDO assignment for slave {0}", slave);
}
}
}
public void RequestPDOMapping(UInt16 slave,UInt16 index,byte subIndex, bool RxPDO) => RequestPDOMapping(new PDOMappingRequest(slave,index,subIndex,RxPDO));
public void RequestPDOMapping(IEnumerable<PDOMappingRequest> mappingRequests) => RequestPDOMapping(mappingRequests.ToArray());
public void RequestPDOMapping(params PDOMappingRequest[] mappingRequests)
{
foreach (PDOMappingRequest mappingRequest in mappingRequests)
this.mappingRequests.Add(mappingRequest);
}
void ScheduleRestart()
{
ThreadPool.QueueUserWorkItem((o)=>{
Stop();
Thread.Sleep(500);
Start();
});
}
public bool ReadSDO(UInt16 slave,UInt16 index, byte subIndex, out byte[] rawValue)
{
rawValue =
new byte[128];
int dataSize;
lock (lockIOMap)
{
dataSize =
ECMBind.ecmbind_sdo_read(slave, index, subIndex, rawValue, rawValue.Length );
if (dataSize <= 0)
{
Logging.Log(LogLevel.ERROR, "ECMBind.ecmbind_sdo_read({0},{1},{2},..,{3}) [{5}] == {4}", slave, index, subIndex, rawValue.Length, rawValue.ToHexString(), dataSize);
throw new IOException();
}
}
rawValue = rawValue.Slice(0, dataSize);
return true;
}
public bool ReadSDOCA(UInt16 slave,UInt16 index, byte subIndex, out byte[] rawValue)
{
rawValue =
new byte[128];
int dataSize;
lock (lockIOMap)
{
dataSize =
ECMBind.ecmbind_sdo_read_ca(slave, index, subIndex, rawValue, rawValue.Length );
if (dataSize <= 0)
{
Logging.Log(LogLevel.ERROR, "ECMBind.ecmbind_sdo_read_ca({0},{1},{2},..,{3}) == {4}", slave, index, subIndex, rawValue.Length, rawValue.ToHexString());
throw new IOException();
}
}
rawValue = rawValue.Slice(0, dataSize);
return true;
}
public bool WriteSDO(UInt16 slave,UInt16 index, byte subIndex, byte[] rawValue)
{
int result;
lock (lockIOMap)
result = ECMBind.ecmbind_sdo_write(slave, index, subIndex, rawValue, rawValue.Length);
//Logging.Log(LogLevel.DEBUG, "ECMaster: WriteSDO({0},{1},{2},{3}) => {4}", slave, index, subIndex, rawValue.ToHexString(), result);
return result > 0;
}
public bool WriteSDOCA(UInt16 slave,UInt16 index, byte subIndex, byte[] rawValue)
{
int result;
lock (lockIOMap)
result = ECMBind.ecmbind_sdo_write_ca(slave, index, subIndex, rawValue, rawValue.Length);
//Logging.Log(LogLevel.DEBUG, "ECMaster: WriteSDOCA({0},{1},{2},{3}) => {4}", slave, index, subIndex, rawValue.ToHexString(), result);
return result > 0;
}
public ECSlaveState ReadSlaveState(int slave) => IOLocked(()=>ECMBind.ecmbind_get_slave_state(slave));
int RequestSlaveState(int slave, ECSlaveState slaveState) => IOLocked(()=>ECMBind.ecmbind_write_slave_state(slave, slaveState));
public bool GetSlave(UInt16 slave_id, out ECSlave slave)
{
if (!slaves.TryGetValue(slave_id, out slave))
{
if (slave_id > CountSlaves)
return false;
slave = new ECSlave(this, slave_id);
slaves.Add(slave_id, slave);
}
return true;
}
public bool GetSDOValue(SDOAddr address, out SDOValue sdoValue) => GetSDOValue(address.Slave, address.Index, address.SubIndex, out sdoValue);
public bool GetSDOValue(UInt16 slave_id, UInt16 index,byte subIndex, out SDOValue sdoValue)
{
sdoValue = null;
return GetSlave(slave_id, out ECSlave slave) && slave.GetDescriptor(index, out SDODescriptor descriptor) && descriptor.GetValue(subIndex, out sdoValue);
}
public bool GetIOmapData(int offset,int length,out byte[] rawData)
{
lock (lockIOMap)
if ((offset >= 0) && (length > 0) && (offset+length < iomap.Length))
{
rawData = iomap.Slice(offset, length);
//Logging.Log(LogLevel.ERROR, "GetIOmapData({0},{1})", offset, length);
return true;
}
Logging.Log(LogLevel.ERROR, "GetIOmapData({0},{1}) failed", offset, length);
rawData = null;
return false;
}
public bool SetIOmapData(int offset,int length,byte[] rawData)
{
lock (lockIOMap)
if ((offset >= 0) && (length > 0) && (offset+length < iomap.Length))
{
//Logging.Log(LogLevel.DEBUG, "ECMaster: SetIOmapData({0},{1},{2})", offset, length, rawData.ToHexString());
Buffer.BlockCopy(rawData, 0, iomap, offset, length);
return true;
}
Logging.Log(LogLevel.ERROR, "SetIOmapData({0},{1}) failed", offset, length);
rawData = null;
return false;
}
public bool GetPDOItem(SDOValue sdoValue, out PDO pdo)
{
foreach (PDO _pdo in pdoMap)
{
if (_pdo.SDOValue.Equals(sdoValue))
{
pdo = _pdo;
return true;
}
}
pdo = null;
return false;
}
public void UpdatePDOMap()
{
List<PDO> pdoList = new List<PDO>();
lock (lockIOMap)
ECMBind.ecmbind_pdo_enumerate((UInt16 slave, UInt16 index, byte subindex, int addr_offset, int addr_bit, int bitlength)=>{
if (addr_bit != 0)
Logging.Log(LogLevel.WARNING, "currently only PDO mappings on byte boundaries are supported");
else
pdoList.Add(new PDO(this,slave, index, subindex){
AddressOffset = addr_offset,
AddressBit = addr_bit,
BitLength = bitlength
});
});
lock (lockIOMap)
{
IOMapPtr = ECMBind.ecmbind_get_iomap();
pdoMap = pdoList;
}
}
T IOLocked<T>(Func<T> f) {
lock (lockIOMap)
return f();
}
}
}