From 0fb7af31c0826596e2e32dd116cba22504773c63 Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Wed, 16 Dec 2020 09:29:40 +0100 Subject: [PATCH] Alpha Commit --- .gitignore | 46 +++ .gitmodules | 3 + build.ln | 24 ++ contrib.soem.make | 28 ++ contrib/SOEM | 1 + libecmbind/ecd_map.c | 258 ++++++++++++ libecmbind/ecmbind.h | 38 ++ libecmbind/libecmbind.c | 220 ++++++++++ libecmbind/libecmbind_descriptors.c | 295 ++++++++++++++ ln.ethercat.service/EthercatService.cs | 61 +++ ln.ethercat.service/Program.cs | 42 ++ .../api/v1/ControllerApiController.cs | 15 + .../api/v1/EthercatApiController.cs | 104 +++++ .../ln.ethercat.service.csproj | 18 + ln.ethercat.sln | 62 +++ ln.ethercat.tests/UnitTest1.cs | 24 ++ ln.ethercat.tests/ln.ethercat.tests.csproj | 16 + ln.ethercat/ECDataTypeConverter.cs | 42 ++ ln.ethercat/ECDataTypes.cs | 34 ++ ln.ethercat/ECMBind.cs | 126 ++++++ ln.ethercat/ECMaster.cs | 379 ++++++++++++++++++ ln.ethercat/ECObjectCodes.cs | 18 + ln.ethercat/ECSlaveState.cs | 18 + ln.ethercat/PDO.cs | 32 ++ ln.ethercat/SDO.cs | 92 +++++ ln.ethercat/SDOAddr.cs | 35 ++ ln.ethercat/SDOCache.cs | 48 +++ ln.ethercat/controller/ControlLoop.cs | 92 +++++ ln.ethercat/controller/Controller.cs | 101 +++++ .../controller/drives/CIA402Controller.cs | 242 +++++++++++ .../controller/drives/DriveController.cs | 88 ++++ ln.ethercat/lib/libecmbind.so | Bin 0 -> 179504 bytes ln.ethercat/ln.ethercat.csproj | 25 ++ 33 files changed, 2627 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 build.ln create mode 100644 contrib.soem.make create mode 160000 contrib/SOEM create mode 100644 libecmbind/ecd_map.c create mode 100644 libecmbind/ecmbind.h create mode 100644 libecmbind/libecmbind.c create mode 100644 libecmbind/libecmbind_descriptors.c create mode 100644 ln.ethercat.service/EthercatService.cs create mode 100644 ln.ethercat.service/Program.cs create mode 100644 ln.ethercat.service/api/v1/ControllerApiController.cs create mode 100644 ln.ethercat.service/api/v1/EthercatApiController.cs create mode 100644 ln.ethercat.service/ln.ethercat.service.csproj create mode 100644 ln.ethercat.sln create mode 100644 ln.ethercat.tests/UnitTest1.cs create mode 100644 ln.ethercat.tests/ln.ethercat.tests.csproj create mode 100644 ln.ethercat/ECDataTypeConverter.cs create mode 100644 ln.ethercat/ECDataTypes.cs create mode 100644 ln.ethercat/ECMBind.cs create mode 100644 ln.ethercat/ECMaster.cs create mode 100644 ln.ethercat/ECObjectCodes.cs create mode 100644 ln.ethercat/ECSlaveState.cs create mode 100644 ln.ethercat/PDO.cs create mode 100644 ln.ethercat/SDO.cs create mode 100644 ln.ethercat/SDOAddr.cs create mode 100644 ln.ethercat/SDOCache.cs create mode 100644 ln.ethercat/controller/ControlLoop.cs create mode 100644 ln.ethercat/controller/Controller.cs create mode 100644 ln.ethercat/controller/drives/CIA402Controller.cs create mode 100644 ln.ethercat/controller/drives/DriveController.cs create mode 100755 ln.ethercat/lib/libecmbind.so create mode 100644 ln.ethercat/ln.ethercat.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..542b474 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover + +*.log +*.log.old +.vscode +.build \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..24e77dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contrib/SOEM"] + path = contrib/SOEM + url = https://git.l--n.de/ln-dotnet/SOEM.git diff --git a/build.ln b/build.ln new file mode 100644 index 0000000..0cf44ab --- /dev/null +++ b/build.ln @@ -0,0 +1,24 @@ +{ + "templates": [ + "dotnet" + ], + "env": { + "NUGET_SOURCE": "https://nexus.niclas-thobaben.de/repository/l--n.de/", + "CONFIGURATION": "Release" + }, + "stages": [ + { + "name": "prepare", + "commands": [ + "dotnet prepare */*.csproj" + ] + }, + { + "name": "native-build", + "priority": 200, + "commands": [ + "sh make -f contrib.soem.make all" + ] + } + ] +} \ No newline at end of file diff --git a/contrib.soem.make b/contrib.soem.make new file mode 100644 index 0000000..db8ba26 --- /dev/null +++ b/contrib.soem.make @@ -0,0 +1,28 @@ +CFLAGS=-fPIC -Icontrib/SOEM/soem -Icontrib/SOEM/osal -Icontrib/SOEM/osal/linux -Icontrib/SOEM/oshw/linux + +SRC_ECMBIND=$(wildcard libecmbind/*.c) +OBJ_ECMBIND=$(SRC_ECMBIND:libecmbind/%.c=.build/ecmbind/%.o) + + +all: DIR libecmbind.so + +clean: + rm -Rf .build/soem .build/ecmbind .build/soem-unpack + +DIR: + mkdir -p .build/ecmbind .build/soem .build/soem-unpack + +.build/soem/libsoem.a: contrib/SOEM/CMakeLists.txt + rm -Rf .build/soem/* + cd .build/soem; CFLAGS=-fPIC cmake ../../contrib/SOEM; make + cd .build/soem-unpack; ar x ../soem/libsoem.a + +libecmbind.so: $(OBJ_ECMBIND) .build/soem/libsoem.a + gcc -shared -o ln.ethercat/lib/libecmbind.so $(OBJ_ECMBIND) $(wildcard .build/soem-unpack/*.o) -lpthread + + + + + +.build/ecmbind/%.o: libecmbind/%.c + gcc -c -o $@ $< $(CFLAGS) diff --git a/contrib/SOEM b/contrib/SOEM new file mode 160000 index 0000000..342ca86 --- /dev/null +++ b/contrib/SOEM @@ -0,0 +1 @@ +Subproject commit 342ca8632c3a495ea9700cc2ea189ca20c12c3e2 diff --git a/libecmbind/ecd_map.c b/libecmbind/ecd_map.c new file mode 100644 index 0000000..195a2e0 --- /dev/null +++ b/libecmbind/ecd_map.c @@ -0,0 +1,258 @@ +#include +#include +#include +#include +#include + +#include "ecmbind.h" + + +extern char ecd_iomap[4096]; +extern int ecd_iomap_size; + +ecd_pdo_entry_t ecd_pdo_map[1024]; +int ecd_pdo_map_length; + + + + +/** Read PDO assign structure */ +int ecd_read_pdoassign(uint16 slave, uint16 PDOassign, int mapoffset, int bitoffset) +{ + ec_ODlistt ODlist; + ec_OElistt OElist; + + uint16 idxloop, nidx, subidxloop, rdat, idx, subidx; + uint8 subcnt; + int wkc, bsize = 0, rdl; + int32 rdat2; + uint8 bitlen, obj_subidx; + uint16 obj_idx; + int abs_offset, abs_bit; + + rdl = sizeof(rdat); rdat = 0; + /* read PDO assign subindex 0 ( = number of PDO's) */ + wkc = ec_SDOread(slave, PDOassign, 0x00, FALSE, &rdl, &rdat, EC_TIMEOUTRXM); + rdat = etohs(rdat); + /* positive result from slave ? */ + if ((wkc > 0) && (rdat > 0)) + { + /* number of available sub indexes */ + nidx = rdat; + bsize = 0; + /* read all PDO's */ + for (idxloop = 1; idxloop <= nidx; idxloop++) + { + rdl = sizeof(rdat); rdat = 0; + /* read PDO assign */ + wkc = ec_SDOread(slave, PDOassign, (uint8)idxloop, FALSE, &rdl, &rdat, EC_TIMEOUTRXM); + /* result is index of PDO */ + idx = etohs(rdat); + if (idx > 0) + { + rdl = sizeof(subcnt); subcnt = 0; + /* read number of subindexes of PDO */ + wkc = ec_SDOread(slave,idx, 0x00, FALSE, &rdl, &subcnt, EC_TIMEOUTRXM); + subidx = subcnt; + /* for each subindex */ + for (subidxloop = 1; subidxloop <= subidx; subidxloop++) + { + rdl = sizeof(rdat2); rdat2 = 0; + /* read SDO that is mapped in PDO */ + wkc = ec_SDOread(slave, idx, (uint8)subidxloop, FALSE, &rdl, &rdat2, EC_TIMEOUTRXM); + rdat2 = etohl(rdat2); + /* extract bitlength of SDO */ + bitlen = LO_BYTE(rdat2); + bsize += bitlen; + obj_idx = (uint16)(rdat2 >> 16); + obj_subidx = (uint8)((rdat2 >> 8) & 0x000000ff); + abs_offset = mapoffset + (bitoffset / 8); + abs_bit = bitoffset % 8; + ODlist.Slave = slave; + ODlist.Index[0] = obj_idx; + OElist.Entries = 0; + wkc = 0; + /* read object entry from dictionary if not a filler (0x0000:0x00) */ + if(obj_idx || obj_subidx) + wkc = ec_readOEsingle(0, obj_subidx, &ODlist, &OElist); + //printf(" [0x%4.4X.%1d] 0x%4.4X:0x%2.2X 0x%2.2X", abs_offset, abs_bit, obj_idx, obj_subidx, bitlen); + + ecd_pdo_map[ecd_pdo_map_length] = (ecd_pdo_entry_t){ + slave, + obj_idx, + obj_subidx, + abs_offset, + abs_bit, + bitlen, + 0, + "" + }; + + if((wkc > 0) && OElist.Entries) + { + ecd_pdo_map[ecd_pdo_map_length].type = OElist.DataType[obj_subidx]; + strncpy(ecd_pdo_map[ecd_pdo_map_length].name, OElist.Name[obj_subidx], 32); + + //printf(" %-12s %s\n", dtype2string(OElist.DataType[obj_subidx]), OElist.Name[obj_subidx]); + } +/* + printf(" [0x%4.4X.%1d] 0x%4.4X:0x%2.2X 0x%2.2X) %s\n", + ecd_pdo_map[ecd_pdo_map_length].addr_offset, + ecd_pdo_map[ecd_pdo_map_length].addr_bit, + ecd_pdo_map[ecd_pdo_map_length].index, + ecd_pdo_map[ecd_pdo_map_length].subindex, + ecd_pdo_map[ecd_pdo_map_length].bitlength, + ecd_pdo_map[ecd_pdo_map_length].name + + ); +*/ + ecd_pdo_map_length++; + + bitoffset += bitlen; + }; + }; + }; + }; + /* return total found bitlength (PDO) */ + return bsize; +} + + +int ecd_read_pdo_map(int slave) +{ + int wkc, rdl; + int retVal = -1; + uint8 nSM, iSM, tSM; + int Tsize, outputs_bo, inputs_bo; + uint8 SMt_bug_add; + + //printf("PDO mapping according to CoE :\n"); + SMt_bug_add = 0; + outputs_bo = 0; + inputs_bo = 0; + rdl = sizeof(nSM); nSM = 0; + /* read SyncManager Communication Type object count */ + wkc = ec_SDOread(slave, ECT_SDO_SMCOMMTYPE, 0x00, FALSE, &rdl, &nSM, EC_TIMEOUTRXM); + /* positive result from slave ? */ + if ((wkc > 0) && (nSM > 2)) + { + /* make nSM equal to number of defined SM */ + nSM--; + /* limit to maximum number of SM defined, if true the slave can't be configured */ + if (nSM > EC_MAXSM) + nSM = EC_MAXSM; + /* iterate for every SM type defined */ + for (iSM = 2 ; iSM <= nSM ; iSM++) + { + rdl = sizeof(tSM); tSM = 0; + /* read SyncManager Communication Type */ + wkc = ec_SDOread(slave, ECT_SDO_SMCOMMTYPE, iSM + 1, FALSE, &rdl, &tSM, EC_TIMEOUTRXM); + if (wkc > 0) + { + if((iSM == 2) && (tSM == 2)) // SM2 has type 2 == mailbox out, this is a bug in the slave! + { + SMt_bug_add = 1; // try to correct, this works if the types are 0 1 2 3 and should be 1 2 3 4 + //printf("Activated SM type workaround, possible incorrect mapping.\n"); + } + if(tSM) + tSM += SMt_bug_add; // only add if SMt > 0 + + if (tSM == 3) // outputs + { + /* read the assign RXPDO */ + //printf(" SM%1d outputs\n addr b index: sub bitl data_type name\n", iSM); + Tsize = ecd_read_pdoassign(slave, ECT_SDO_PDOASSIGN + iSM, (int)(ec_slave[slave].outputs - (uint8 *)&ecd_iomap[0]), outputs_bo ); + outputs_bo += Tsize; + } + if (tSM == 4) // inputs + { + /* read the assign TXPDO */ + //printf(" SM%1d inputs\n addr b index: sub bitl data_type name\n", iSM); + Tsize = ecd_read_pdoassign(slave, ECT_SDO_PDOASSIGN + iSM, (int)(ec_slave[slave].inputs - (uint8 *)&ecd_iomap[0]), inputs_bo ); + inputs_bo += Tsize; + } + } + } + } + + /* found some I/O bits ? */ + if ((outputs_bo > 0) || (inputs_bo > 0)) + retVal = 0; + return retVal; +} + +int ecd_extract_map_value(int offset,int firstbit,int bitlength,char *buffer) +{ + int n; + int bytelength = bitlength ? (7 + firstbit + bitlength) / 8 : 0; + + memcpy(buffer, &ecd_iomap[offset], bytelength); +/* + fprintf(stderr, "IM: "); + print_hex(buffer, bytelength); +*/ + buffer[bytelength] = 0; + + if (firstbit) + { + for (n=0;n> firstbit) & (0xFFu >> firstbit) | (buffer[n+1] << (8-firstbit))); + } + buffer[n] = ((buffer[n] >> firstbit) & (0xFF >> firstbit)); + } + +/* fprintf(stderr, "\nFIN: "); + print_hex(buffer, bytelength); + fprintf(stderr, "\n"); + fflush(stderr); +*/ + return (bitlength); +} + +int ecd_insert_map_value(int offset,int firstbit,int bitlength,char *buffer) +{ + int n; + int bytelength = bitlength ? (7 + firstbit + bitlength) / 8 : 0; + + char* im = malloc(bytelength); + if (!im) + return -1; + + im[bytelength-1] = 0x00; + memcpy(im, buffer, bitlength / 8); + +/* fprintf(stderr, "IM: "); + print_hex(im, bytelength); +*/ + if (firstbit) + { + for (n=bytelength-1;n > 0;n++) + { + im[n-1] = ((im[n] << firstbit) | (im[n-1] >> (8-firstbit))); + } + im[n] = ((im[n] >> firstbit) & (0xFF >> firstbit)); + } +/* + fprintf(stderr, "\nFIN: "); + print_hex(im, bytelength); + fprintf(stderr, "\n"); + fflush(stderr); +*/ + if (firstbit) + { +/* fprintf(stderr, "FIXME: unaligned bits not supported yet!\n"); + fflush(stderr); +*/ + } else { +/* fprintf(stderr, "Inserting at %d = ", offset); + print_hex(im, bytelength); +*/ + memcpy(&ecd_iomap[offset], im, bytelength); + } + + return (bitlength); +} + + + diff --git a/libecmbind/ecmbind.h b/libecmbind/ecmbind.h new file mode 100644 index 0000000..77a8087 --- /dev/null +++ b/libecmbind/ecmbind.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +extern int TIMEOUT_PROCESSDATA; + +typedef struct +{ + int slave; + short index; + char subindex; + int addr_offset; + int addr_bit; + int bitlength; + int type; + char name[64]; +} ecd_pdo_entry_t; + +typedef struct { + int slave; + uint16_t index; + uint16_t datatype; + uint8_t objectcode; + uint8_t maxsub; + + char name[128]; +} dto_servicedescriptor_t; + +typedef void (*cb_enum_indeces_t)(int slave, int index); +typedef void (*cb_enum_sdo_descriptors_t)(int slave, int index, uint16_t dataType, uint16_t objectCode, int sub, char *name); +typedef void (*cb_enum_pdo)(uint16_t slave, uint16_t index, char subindex, int addr_offset, int addr_bit, int bitlength); + +extern ecd_pdo_entry_t ecd_pdo_map[1024]; +extern int ecd_pdo_map_length; + + +int ecd_read_pdo_map(int slave); + diff --git a/libecmbind/libecmbind.c b/libecmbind/libecmbind.c new file mode 100644 index 0000000..2ca46a2 --- /dev/null +++ b/libecmbind/libecmbind.c @@ -0,0 +1,220 @@ +#include +#include +#include + +#include "ecmbind.h" + +int TIMEOUT_PROCESSDATA = 2000; + +char ecd_iomap[4096]; +int ecd_iomap_size; +int ecd_expected_wkc_size; + +int ecmbind_version(char *versionString) +{ + strcpy(versionString, "ecmbind/0.1.0"); + return 0x00010000; +} + +int ecmbind_initialize(char *ifname) +{ + return ec_init(ifname); +} + +int ecmbind_config_init() +{ + ec_config_init(FALSE); + return ec_slavecount; +} + +void* ecmbind_get_iomap() { return ecd_iomap; } +int ecmbind_get_expected_wkc_size() { return ecd_expected_wkc_size; } + +int ecmbind_config_map() +{ + ecd_pdo_map_length = 0; + + ecd_iomap_size = ec_config_map(&ecd_iomap); + //ec_configdc(); + ecd_expected_wkc_size = (ec_group[0].outputsWKC * 2) + ec_group[0].inputsWKC; + + for (int slave=1;slave<=ec_slavecount;slave++) + ecd_read_pdo_map(slave); + + return ecd_iomap_size; +} + +uint16 ecmbind_read_state() +{ + return ec_readstate(); +} + +uint16 ecmbind_get_slave_state(int slave) +{ + return ec_slave[slave].state; +} + + +int ecmbind_write_slave_state(int slave, uint16 state) +{ + ec_slave[slave].state = state; + return ec_writestate(slave); +} + +uint16 ecmbind_request_state(int slave, uint16 reqState, int timeout) +{ + return ec_statecheck(slave, reqState, timeout * 1000); +} + +int ecmbind_processdata() +{ + ec_send_processdata(); + int wkc = ec_receive_processdata(EC_TIMEOUTRET); + return wkc; +} + +int ecmbind_processdata2(char *managed_iomap, int size) +{ + if (size != ecd_iomap_size) + return -1; + + memcpy( ecd_iomap, managed_iomap, ecd_iomap_size); + + ec_send_processdata(); + int wkc = ec_receive_processdata(EC_TIMEOUTRET); + + memcpy( managed_iomap, ecd_iomap, ecd_iomap_size); + + return wkc; +} + +int ecmbind_recover() +{ + int slave = 0; + ec_group[0].docheckstate = FALSE; + ec_readstate(); + for (slave = 1; slave <= ec_slavecount; slave++) + { + if ((ec_slave[slave].group == 0) && (ec_slave[slave].state != EC_STATE_OPERATIONAL)) + { + ec_group[0].docheckstate = TRUE; + if (ec_slave[slave].state == (EC_STATE_SAFE_OP + EC_STATE_ERROR)) + { + printf("ERROR : slave %d is in SAFE_OP + ERROR, attempting ack.\n", slave); + ec_slave[slave].state = (EC_STATE_SAFE_OP + EC_STATE_ACK); + ec_writestate(slave); + } + else if(ec_slave[slave].state == EC_STATE_SAFE_OP) + { + printf("WARNING : slave %d is in SAFE_OP, change to OPERATIONAL.\n", slave); + ec_slave[slave].state = EC_STATE_OPERATIONAL; + ec_writestate(slave); + } + else if(ec_slave[slave].state > EC_STATE_NONE) + { + if (ec_reconfig_slave(slave, TIMEOUT_PROCESSDATA)) + { + ec_slave[slave].islost = FALSE; + printf("MESSAGE : slave %d reconfigured\n",slave); + } + } + else if(!ec_slave[slave].islost) + { + /* re-check state */ + ec_statecheck(slave, EC_STATE_OPERATIONAL, TIMEOUT_PROCESSDATA); + if (ec_slave[slave].state == EC_STATE_NONE) + { + ec_slave[slave].islost = TRUE; + printf("ERROR : slave %d lost\n",slave); + } + } + } + + if (ec_slave[slave].islost) + { + if(ec_slave[slave].state == EC_STATE_NONE) + { + if (ec_recover_slave(slave, TIMEOUT_PROCESSDATA)) + { + ec_slave[slave].islost = FALSE; + printf("MESSAGE : slave %d recovered\n",slave); + } + } + else + { + ec_slave[slave].islost = FALSE; + printf("MESSAGE : slave %d found\n",slave); + } + } + } + + if(!ec_group[0].docheckstate) + { + printf("OK : all slaves resumed OPERATIONAL.\n"); + ecd_expected_wkc_size = (ec_group[0].outputsWKC * 2) + ec_group[0].inputsWKC; + return ecd_expected_wkc_size; + } + return -1; +} + +int ecmbind_get_pdo_entries_length() { return ecd_pdo_map_length; } +int ecmbind_get_pdo_entries(ecd_pdo_entry_t* table, int length) +{ + if (length < ecd_pdo_map_length) + return -1; + + int tableSize = (sizeof(ecd_pdo_entry_t)* ecd_pdo_map_length); + memcpy(table, ecd_pdo_map, tableSize); + + return ecd_pdo_map_length; +} + +int ecmbind_pdo_read(int slave, int index, int subindex, char* buffer, int size) +{ + return -1; +} +int ecmbind_pdo_write(int slave, int index, int subindex, char* buffer, int size) +{ + return -1; +} + +int ecmbind_sdo_read(int slave, int index, int subindex, char* buffer, int size) +{ + int wkc = ec_SDOread(slave, index, subindex, FALSE, &size, buffer, 100000); + if (wkc <= 0) + return -1; + return size; +} +int ecmbind_sdo_write(int slave, int index, int subindex, char* buffer, int size) +{ + return -1; +} + +/* +typedef void (*cb_enum_pdo)(int slave, int index, int subindex, int addr_offset, int addr_bit, int bitlength); +*/ +int ecmbind_pdo_enumerate(cb_enum_pdo cb) +{ + printf("libecmbind: ecmbind_pdo_enumerate(): enumerating %d PDOs\n", ecd_pdo_map_length); + for (int n=0;n ecd_iomap_size) + return -1; + memcpy(buffer, &ecd_iomap[offset], length); + return length; +} +int ecmbind_iomap_set(int offset, char* buffer, int length) +{ + if (offset + length > ecd_iomap_size) + return -1; + + memcpy(&ecd_iomap[offset], buffer, length); +} + + diff --git a/libecmbind/libecmbind_descriptors.c b/libecmbind/libecmbind_descriptors.c new file mode 100644 index 0000000..34a0ca7 --- /dev/null +++ b/libecmbind/libecmbind_descriptors.c @@ -0,0 +1,295 @@ +#include +#include +#include + +#include +#include "ecmbind.h" + +/** SDO service structure */ +PACKED_BEGIN +typedef struct PACKED +{ + ec_mbxheadert MbxHeader; + uint16 CANOpen; + uint8 Opcode; + uint8 Reserved; + uint16 Fragments; + union + { + uint8 bdata[0x200]; /* variants for easy data access */ + uint16 wdata[0x100]; + uint32 ldata[0x80]; + }; +} ec_SDOservicet; +PACKED_END + + + +static void ecx_SDOinfoerror(ecx_contextt *context, uint16 Slave, uint16 Index, uint8 SubIdx, int32 AbortCode) +{ + ec_errort Ec; + + memset(&Ec, 0, sizeof(Ec)); + Ec.Slave = Slave; + Ec.Index = Index; + Ec.SubIdx = SubIdx; + *(context->ecaterror) = TRUE; + Ec.Etype = EC_ERR_TYPE_SDOINFO_ERROR; + Ec.AbortCode = AbortCode; + ecx_pusherror(context, &Ec); +} + + +int ecmbind_enumerate_servicedescriptors(int Slave, cb_enum_indeces_t cb) +{ + ecx_contextt *context = &ecx_context; + ec_SDOservicet *SDOp, *aSDOp; + ec_mbxbuft MbxIn, MbxOut; + int wkc; + uint16 x, n, i, sp, offset; + boolean stop; + uint8 cnt; + boolean First; + + if (cb == NULL) + return -1; + + ec_clearmbx(&MbxIn); + /* clear pending out mailbox in slave if available. Timeout is set to 0 */ + wkc = ecx_mbxreceive(context, Slave, &MbxIn, 0); + ec_clearmbx(&MbxOut); + aSDOp = (ec_SDOservicet*)&MbxIn; + SDOp = (ec_SDOservicet*)&MbxOut; + SDOp->MbxHeader.length = htoes(0x0008); + SDOp->MbxHeader.address = htoes(0x0000); + SDOp->MbxHeader.priority = 0x00; + /* Get new mailbox counter value */ + cnt = ec_nextmbxcnt(context->slavelist[Slave].mbx_cnt); + context->slavelist[Slave].mbx_cnt = cnt; + SDOp->MbxHeader.mbxtype = ECT_MBXT_COE + (cnt << 4); /* CoE */ + SDOp->CANOpen = htoes(0x000 + (ECT_COES_SDOINFO << 12)); /* number 9bits service upper 4 bits */ + SDOp->Opcode = ECT_GET_ODLIST_REQ; /* get object description list request */ + SDOp->Reserved = 0; + SDOp->Fragments = 0; /* fragments left */ + SDOp->wdata[0] = htoes(0x01); /* all objects */ + /* send get object description list request to slave */ + wkc = ecx_mbxsend(context, Slave, &MbxOut, EC_TIMEOUTTXM); + /* mailbox placed in slave ? */ + if (wkc > 0) + { + x = 0; + sp = 0; + First = TRUE; + offset = 1; /* offset to skip info header in first frame, otherwise set to 0 */ + do + { + stop = TRUE; /* assume this is last iteration */ + ec_clearmbx(&MbxIn); + /* read slave response */ + wkc = ecx_mbxreceive(context, Slave, &MbxIn, EC_TIMEOUTRXM); + /* got response ? */ + if (wkc > 0) + { + /* response should be CoE and "get object description list response" */ + if (((aSDOp->MbxHeader.mbxtype & 0x0f) == ECT_MBXT_COE) && + ((aSDOp->Opcode & 0x7f) == ECT_GET_ODLIST_RES)) + { + if (First) + { + /* extract number of indexes from mailbox data size */ + n = (etohs(aSDOp->MbxHeader.length) - (6 + 2)) / 2; + } + else + { + /* extract number of indexes from mailbox data size */ + n = (etohs(aSDOp->MbxHeader.length) - 6) / 2; + } + + /* extract indexes one by one */ + for (i = 0; i < n; i++) + cb(Slave, etohs(aSDOp->wdata[i + offset])); + + sp += n; + /* check if more fragments will follow */ + if (aSDOp->Fragments > 0) + { + stop = FALSE; + } + First = FALSE; + offset = 0; + } + /* got unexpected response from slave */ + else + { + if ((aSDOp->Opcode & 0x7f) == ECT_SDOINFO_ERROR) /* SDO info error received */ + { + ecx_SDOinfoerror(context, Slave, 0, 0, etohl(aSDOp->ldata[0])); + stop = TRUE; + } + else + { + ecx_packeterror(context, Slave, 0, 0, 1); /* Unexpected frame returned */ + } + wkc = 0; + x += 20; + } + } + x++; + } + while ((x <= 128) && !stop); + } + return wkc; +} + +int ecmbind_read_objectdescription(int Slave, int index, cb_enum_sdo_descriptors_t cb) +{ + ecx_contextt *context = &ecx_context; + ec_SDOservicet *SDOp, *aSDOp; + int wkc; + uint16 n; + ec_mbxbuft MbxIn, MbxOut; + uint8 cnt; + char temp[128]; + + ec_clearmbx(&MbxIn); + /* clear pending out mailbox in slave if available. Timeout is set to 0 */ + wkc = ecx_mbxreceive(context, Slave, &MbxIn, 0); + ec_clearmbx(&MbxOut); + aSDOp = (ec_SDOservicet*)&MbxIn; + SDOp = (ec_SDOservicet*)&MbxOut; + SDOp->MbxHeader.length = htoes(0x0008); + SDOp->MbxHeader.address = htoes(0x0000); + SDOp->MbxHeader.priority = 0x00; + /* Get new mailbox counter value */ + cnt = ec_nextmbxcnt(context->slavelist[Slave].mbx_cnt); + context->slavelist[Slave].mbx_cnt = cnt; + SDOp->MbxHeader.mbxtype = ECT_MBXT_COE + (cnt << 4); /* CoE */ + SDOp->CANOpen = htoes(0x000 + (ECT_COES_SDOINFO << 12)); /* number 9bits service upper 4 bits */ + SDOp->Opcode = ECT_GET_OD_REQ; /* get object description request */ + SDOp->Reserved = 0; + SDOp->Fragments = 0; /* fragments left */ + SDOp->wdata[0] = htoes(index); /* Data of Index */ + /* send get object description request to slave */ + wkc = ecx_mbxsend(context, Slave, &MbxOut, EC_TIMEOUTTXM); + /* mailbox placed in slave ? */ + if (wkc > 0) + { + ec_clearmbx(&MbxIn); + /* read slave response */ + wkc = ecx_mbxreceive(context, Slave, &MbxIn, EC_TIMEOUTRXM); + /* got response ? */ + if (wkc > 0) + { + if (((aSDOp->MbxHeader.mbxtype & 0x0f) == ECT_MBXT_COE) && + ((aSDOp->Opcode & 0x7f) == ECT_GET_OD_RES)) + { + n = (etohs(aSDOp->MbxHeader.length) - 12); /* length of string(name of object) */ + if (n > sizeof(temp)-1) + n = sizeof(temp)-1; + strncpy(temp , (char *)&aSDOp->bdata[6], n); + temp[n] = 0x00; + + cb(Slave, index, etohs(aSDOp->wdata[1]), aSDOp->bdata[5], aSDOp->bdata[4], temp); + } + /* got unexpected response from slave */ + else + { + if (((aSDOp->Opcode & 0x7f) == ECT_SDOINFO_ERROR)) /* SDO info error received */ + { + ecx_SDOinfoerror(context, Slave, index, 0, etohl(aSDOp->ldata[0])); + } + else + { + ecx_packeterror(context, Slave, index, 0, 1); /* Unexpected frame returned */ + } + wkc = 0; + } + } + } + + return wkc; +} + +int ecmbind_read_objectdescription_entry(uint16_t slave, uint16_t index, uint16_t sub, cb_enum_sdo_descriptors_t cb) +{ + ecx_contextt *context = &ecx_context; + ec_SDOservicet *SDOp, *aSDOp; + int wkc; + int16 n; + ec_mbxbuft MbxIn, MbxOut; + uint8 cnt; + char temp[128]; + + wkc = 0; + ec_clearmbx(&MbxIn); + /* clear pending out mailbox in slave if available. Timeout is set to 0 */ + wkc = ecx_mbxreceive(context, slave, &MbxIn, 0); + ec_clearmbx(&MbxOut); + aSDOp = (ec_SDOservicet*)&MbxIn; + SDOp = (ec_SDOservicet*)&MbxOut; + SDOp->MbxHeader.length = htoes(0x000a); + SDOp->MbxHeader.address = htoes(0x0000); + SDOp->MbxHeader.priority = 0x00; + /* Get new mailbox counter value */ + cnt = ec_nextmbxcnt(context->slavelist[slave].mbx_cnt); + context->slavelist[slave].mbx_cnt = cnt; + SDOp->MbxHeader.mbxtype = ECT_MBXT_COE + (cnt << 4); /* CoE */ + SDOp->CANOpen = htoes(0x000 + (ECT_COES_SDOINFO << 12)); /* number 9bits service upper 4 bits */ + SDOp->Opcode = ECT_GET_OE_REQ; /* get object entry description request */ + SDOp->Reserved = 0; + SDOp->Fragments = 0; /* fragments left */ + SDOp->wdata[0] = htoes(index); /* index */ + SDOp->bdata[2] = sub; /* Subindex */ + SDOp->bdata[3] = 1 + 2 + 4; /* get access rights, object category, PDO */ + /* send get object entry description request to slave */ + wkc = ecx_mbxsend(context, slave, &MbxOut, EC_TIMEOUTTXM); + /* mailbox placed in slave ? */ + if (wkc > 0) + { + ec_clearmbx(&MbxIn); + /* read slave response */ + wkc = ecx_mbxreceive(context, slave, &MbxIn, EC_TIMEOUTRXM); + /* got response ? */ + if (wkc > 0) + { + if (((aSDOp->MbxHeader.mbxtype & 0x0f) == ECT_MBXT_COE) && + ((aSDOp->Opcode & 0x7f) == ECT_GET_OE_RES)) + { + n = (etohs(aSDOp->MbxHeader.length) - 16); /* length of string(name of object) */ + if (n > sizeof(temp)-1) + n = sizeof(temp)-1; + + if (n < 0 ) + n = 0; + + strncpy(temp , (char *)&aSDOp->wdata[5], n); + temp[n] = 0x00; /* string terminator */ + + cb(slave, index, (ushort)etohs(aSDOp->wdata[2]), 0, sub, temp); + +/* + pOElist->ValueInfo[sub] = aSDOp->bdata[3]; + pOElist->DataType[sub] = etohs(aSDOp->wdata[2]); + pOElist->BitLength[sub] = etohs(aSDOp->wdata[3]); + pOElist->ObjAccess[sub] = etohs(aSDOp->wdata[4]); +*/ + } + + /* got unexpected response from slave */ + else + { + if (((aSDOp->Opcode & 0x7f) == ECT_SDOINFO_ERROR)) /* SDO info error received */ + { + ecx_SDOinfoerror(context, slave, index, sub, etohl(aSDOp->ldata[0])); + } + else + { + ecx_packeterror(context, slave, index, sub, 1); /* Unexpected frame returned */ + } + wkc = 0; + } + } + } + + return wkc; +} diff --git a/ln.ethercat.service/EthercatService.cs b/ln.ethercat.service/EthercatService.cs new file mode 100644 index 0000000..9f17db9 --- /dev/null +++ b/ln.ethercat.service/EthercatService.cs @@ -0,0 +1,61 @@ + + +using System.Threading; +using ln.ethercat.service.api.v1; +using ln.http; +using ln.http.router; +using ln.logging; +using ln.type; + +namespace ln.ethercat.service +{ + public class EthercatService + { + + public ECMaster ECMaster { get; } + + + HTTPServer httpServer; + LoggingRouter httpLoggingRouter; + SimpleRouter httpRouter; + + EthercatApiController apiController; + + + public EthercatService(string interfaceName) + { + ECMaster = new ECMaster(interfaceName); + + Initialize(); + } + + void Initialize() + { + httpRouter = new SimpleRouter(); + httpLoggingRouter = new LoggingRouter(httpRouter); + + apiController = new EthercatApiController(ECMaster); + httpRouter.AddSimpleRoute("/api/v1/*", apiController); + + httpServer = new HTTPServer(httpLoggingRouter); + httpServer.AddEndpoint(new Endpoint(IPv6.ANY, 7676)); + + } + + + public void Start() + { + httpServer.Start(); + + ECMaster.Start(); + + while (ECMaster.MasterState != ECMasterState.RUNNING) + Thread.Sleep(100); + + Logging.Log(LogLevel.INFO,"ECMaster is ready (ExpectedWorkCounter={0})", ECMaster.ExpectedWorkCounter); + } + + + + } +} \ No newline at end of file diff --git a/ln.ethercat.service/Program.cs b/ln.ethercat.service/Program.cs new file mode 100644 index 0000000..b787dab --- /dev/null +++ b/ln.ethercat.service/Program.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; +using System.Threading; +using ln.logging; +using ln.type; + +namespace ln.ethercat.service +{ + class Program + { + static void Main(string[] args) + { + Logging.Log(LogLevel.INFO, ".NET EtherCAT service host"); + + StringBuilder versionString = new StringBuilder(1024); + ECMBind.ecmbind_version(versionString); + + Logging.Log(LogLevel.INFO, "ECMBind version: {0}", versionString.ToString()); + + EthercatService ethercatService = new EthercatService(args[0]); + ethercatService.Start(); + +/* + while (true) + { + Thread.Sleep(100); + for (int n=1;n <= ecMaster.CountSlaves;n++) + { + //Logging.Log(LogLevel.DEBUG, "Slave {0} is in state {1}", n, ecMaster.ReadSlaveState(n)); + if (ecMaster.ReadSDO(new DOAddr(n, 0x1000),out byte[] typeBytes)) + { + Logging.Log(LogLevel.DEBUG, "Slave {0} has type {1}", n, typeBytes.ToHexString()); + } else { + Logging.Log(LogLevel.DEBUG, "ReadSDO() failed"); + } + } + } + +*/ + } + } +} diff --git a/ln.ethercat.service/api/v1/ControllerApiController.cs b/ln.ethercat.service/api/v1/ControllerApiController.cs new file mode 100644 index 0000000..7b038b3 --- /dev/null +++ b/ln.ethercat.service/api/v1/ControllerApiController.cs @@ -0,0 +1,15 @@ + + +using ln.http.api; + +namespace ln.ethercat.service.api.v1 +{ + + public class ControllerApiController : WebApiController + { + + + + } + +} \ No newline at end of file diff --git a/ln.ethercat.service/api/v1/EthercatApiController.cs b/ln.ethercat.service/api/v1/EthercatApiController.cs new file mode 100644 index 0000000..66b1ad7 --- /dev/null +++ b/ln.ethercat.service/api/v1/EthercatApiController.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Runtime.CompilerServices; +using ln.http; +using ln.http.api; +using ln.http.api.attributes; +using ln.json; +using ln.json.mapping; +using ln.logging; + +namespace ln.ethercat.service.api.v1 +{ + + public class EthercatApiController : WebApiController + { + ECMaster ECMaster; + public EthercatApiController(ECMaster ecMaster) + { + ECMaster = ecMaster; + } + + [GET("/slaves")] + public int GetSlaveCount() => ECMaster.CountSlaves; + + [GET("/slaves/:slave/sdo/:index/:subindex")] + [GET("/slaves/:slave/sdo/:index")] + public HttpResponse ReadSDO(int slave,int index,int subindex = 0) + { + if (ECMaster.GetSDO(new SDOAddr(slave, index, subindex), out SDO sdo)) + { + Logging.Log(LogLevel.DEBUG, "SDO updated: {0}", sdo); + return HttpResponse + .OK() + .Content(JSONMapper.DefaultMapper.ToJson(sdo)) + .ContentType("application/json") + ; + } + return HttpResponse.RequestTimeout().Content("master could not read from slave"); + } + + [GET("/slaves/:slave/sdo")] + public SDO[] GetServiceDescriptors(int slave) + { + return ECMaster.GetSDOs(slave).ToArray(); + } + + [GET("/slaves/:slave/state")] + public HttpResponse GetEthercatState(int slave) => HttpResponse.OK().Content(ECMaster.ReadSlaveState(slave).ToString()); + + [POST("/slaves/:slave/state")] + public HttpResponse RequestEthercatState(int slave, ECSlaveState state) + { + if (ECMaster.RequestSlaveState(slave, state) > 0) + return HttpResponse.NoContent(); + return HttpResponse.GatewayTimeout(); + } + + + [GET("/master/state")] + public HttpResponse GetFullState() + { + JSONArray slaveStates = new JSONArray(); + + for (int slave=1; slave <= ECMaster.CountSlaves; slave++) + { + slaveStates.Add(new JSONObject() + .Add("id", new JSONNumber(slave)) + .Add("state", new JSONString(ECMaster.ReadSlaveState(slave).ToString())) + ); + } + + JSONObject fullState = new JSONObject() + .Add("bus_state", new JSONString(ECMaster.EthercatState.ToString())) + .Add("slaves", slaveStates) + ; + + return HttpResponse.OK().Content(fullState); + } + + [GET("/master/pdomap")] + public PDO[] GetPDOMap() => ECMaster.GetPDOMap(); + + [GET("/master/pdos")] + public SDO[] GetPDOs() => ECMaster.GetPDOMap().Select((pdo)=>pdo.SDO).ToArray(); + + [POST("/master/action")] + public HttpResponse PostMasterAction(string action) + { + switch (action) + { + case "stop": + ECMaster.Stop(); + return HttpResponse.NoContent(); + case "start": + ECMaster.Start(); + return HttpResponse.NoContent(); + default: + return HttpResponse.BadRequest().Content(string.Format("{0} is no valid action", action)); + } + } + + + } + +} \ No newline at end of file diff --git a/ln.ethercat.service/ln.ethercat.service.csproj b/ln.ethercat.service/ln.ethercat.service.csproj new file mode 100644 index 0000000..14892f2 --- /dev/null +++ b/ln.ethercat.service/ln.ethercat.service.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + diff --git a/ln.ethercat.sln b/ln.ethercat.sln new file mode 100644 index 0000000..d37ec3a --- /dev/null +++ b/ln.ethercat.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ethercat", "ln.ethercat\ln.ethercat.csproj", "{CA065CFA-9690-4276-8234-A82912ABF7FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ethercat.tests", "ln.ethercat.tests\ln.ethercat.tests.csproj", "{3E509EF8-48F8-4C25-8E9A-B5BDE025A422}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ethercat.service", "ln.ethercat.service\ln.ethercat.service.csproj", "{6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|x64.Build.0 = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Debug|x86.Build.0 = Debug|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|Any CPU.Build.0 = Release|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|x64.ActiveCfg = Release|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|x64.Build.0 = Release|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|x86.ActiveCfg = Release|Any CPU + {CA065CFA-9690-4276-8234-A82912ABF7FD}.Release|x86.Build.0 = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|x64.Build.0 = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Debug|x86.Build.0 = Debug|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|Any CPU.Build.0 = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|x64.ActiveCfg = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|x64.Build.0 = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|x86.ActiveCfg = Release|Any CPU + {3E509EF8-48F8-4C25-8E9A-B5BDE025A422}.Release|x86.Build.0 = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|x64.Build.0 = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Debug|x86.Build.0 = Debug|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|Any CPU.Build.0 = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|x64.ActiveCfg = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|x64.Build.0 = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|x86.ActiveCfg = Release|Any CPU + {6F8D4B47-2ECB-416A-85DB-E84B6BC295B1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ln.ethercat.tests/UnitTest1.cs b/ln.ethercat.tests/UnitTest1.cs new file mode 100644 index 0000000..b8aff6e --- /dev/null +++ b/ln.ethercat.tests/UnitTest1.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; +using System.Text; +using NUnit.Framework; + +namespace ln.ethercat.tests +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + StringBuilder versionString = new StringBuilder(1024); + Assert.AreEqual(0x00010000, ECMBind.ecmbind_version(versionString)); + Console.WriteLine("ECMBind Version: {0}", versionString.ToString()); + Assert.Pass(); + } + } +} \ No newline at end of file diff --git a/ln.ethercat.tests/ln.ethercat.tests.csproj b/ln.ethercat.tests/ln.ethercat.tests.csproj new file mode 100644 index 0000000..f687b1f --- /dev/null +++ b/ln.ethercat.tests/ln.ethercat.tests.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + diff --git a/ln.ethercat/ECDataTypeConverter.cs b/ln.ethercat/ECDataTypeConverter.cs new file mode 100644 index 0000000..5e9af7f --- /dev/null +++ b/ln.ethercat/ECDataTypeConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; +using ln.type; + +namespace ln.ethercat +{ + public static class ECDataTypeConverter + { + public static object ConvertFromEthercat(ECDataTypes dataType, byte[] rawData) + { + if (rawData == null) + return null; + + int offset = 0; + + switch (dataType) + { + case ECDataTypes.NONE: + return null; + case ECDataTypes.REAL: + return rawData.GetSingle(ref offset, Endianess.BIG); + case ECDataTypes.LREAL: + return rawData.GetDouble(ref offset, Endianess.BIG); + case ECDataTypes.STRING: + return Encoding.ASCII.GetString(rawData); + case ECDataTypes.INT: + return rawData.GetShort(ref offset, Endianess.BIG); + case ECDataTypes.DINT: + return rawData.GetInt(ref offset, Endianess.BIG); + case ECDataTypes.UINT: + return rawData.GetUShort(ref offset, Endianess.BIG); + case ECDataTypes.UDINT: + return rawData.GetUInt(ref offset, Endianess.BIG); + case ECDataTypes.USINT: + return rawData[offset]; + default: + return rawData; + } + } + + } +} \ No newline at end of file diff --git a/ln.ethercat/ECDataTypes.cs b/ln.ethercat/ECDataTypes.cs new file mode 100644 index 0000000..5b48983 --- /dev/null +++ b/ln.ethercat/ECDataTypes.cs @@ -0,0 +1,34 @@ +namespace ln.ethercat +{ + public enum ECDataTypes : ushort + { + NONE = 0, + BOOL = 0x0001, BIT = 0x0001, + SINT = 0x0002, + INT = 0x0003, + DINT = 0x0004, + USINT = 0x0005, + UINT = 0x0006, + UDINT = 0x0007, + REAL = 0x0008, + VISIBLE_STRING = 0x0009, + STRING = 0x0009, + OCTET_STRING = 0x000A, + ARRAY_OF_BYTE = 0x000A, + UNICODE_STRING = 0x000B, + TIME_OF_DAY = 0x000C, + TIME_DIFFERENCE = 0x000D, + DOMAIN = 0x000F, + INTEGER24 = 0x0010, + UNSIGNED40 = 0x0018, + UNSIGNED48 = 0x0019, + UNSIGNED56 = 0x001A, + UNSIGNED64 = 0x001B, + + ARRAY_OF_UINT = 0x001F, + LREAL = 0x0020, + LINT = 0x0260, + ULINT = 0x0261, + BYTE = 0x0262 + } +} diff --git a/ln.ethercat/ECMBind.cs b/ln.ethercat/ECMBind.cs new file mode 100644 index 0000000..7d717c9 --- /dev/null +++ b/ln.ethercat/ECMBind.cs @@ -0,0 +1,126 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace ln.ethercat +{ + + + [StructLayout(LayoutKind.Sequential)] + public struct PDOEntry + { + public int slave; + public short index; + public byte subindex; + public int addr_offset; + public int addr_bit; + public int bitlength; + public int type; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public StringBuilder name; + + + public override string ToString() + { + return string.Format("PDOEntry(slave={0},index={1},subindex={2},addr_offset={3},addr_bit={4},bitlength={5},type={6})", slave,index,subindex,addr_offset,addr_bit,bitlength,type); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct dto_servicedescriptor + { + public int slave; + public UInt16 index; + public UInt16 datatype; + public UInt16 objectcode; + public byte maxsub; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public StringBuilder name; + } + + public delegate void cb_enum_indeces(int slave, int index); + public delegate void cb_enum_sdo_descriptors(int slave, int index, ECDataTypes dataType, ECObjectCodes objectCode, int maxsub, String name); + public delegate void cb_enum_pdo(UInt16 slave, UInt16 index, byte subindex, int addr_offset, int addr_bit, int bitlength); + + + public static class ECMBind + { + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_version(StringBuilder versionString); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_initialize(String ifname); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_config_init(); + + [DllImport("lib/libecmbind.so")] + public static extern IntPtr ecmbind_get_iomap(); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_get_expected_wkc_size(); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_config_map(); + + [DllImport("lib/libecmbind.so")] + public static extern ECSlaveState ecmbind_read_state(); + + [DllImport("lib/libecmbind.so")] + public static extern ECSlaveState ecmbind_get_slave_state(int slave); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_write_slave_state(int slave, ECSlaveState state); + + [DllImport("lib/libecmbind.so")] + public static extern ECSlaveState ecmbind_request_state(int slave, ECSlaveState reqState, int timeout); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_processdata(); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_recover(); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecd_read_pdo_map(Int32 slave); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_get_pdo_entries_length(); + + [DllImport("lib/libecmbind.so")] + public static extern Int32 ecmbind_get_pdo_entries(ref PDOEntry[] table, int length); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_pdo_read(int slave, int index, int subindex, byte[] buffer, int size); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_pdo_write(int slave, int index, int subindex, byte[] buffer, int size); + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_sdo_read(int slave, int index, int subindex, byte[] buffer, int size); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_sdo_write(int slave, int index, int subindex, byte[] buffer, int size); + + + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_enumerate_servicedescriptors(int Slave, cb_enum_indeces cb); + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_read_objectdescription(int slave, int index, cb_enum_sdo_descriptors cb); + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_read_objectdescription_entry(UInt16 slave, UInt16 index, UInt16 sub, cb_enum_sdo_descriptors cb); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_pdo_enumerate(cb_enum_pdo cb); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_iomap_get(int offset, byte[] buffer, int length); + + [DllImport("lib/libecmbind.so")] + public static extern int ecmbind_iomap_set(int offset, byte[] buffer, int length); + + } +} diff --git a/ln.ethercat/ECMaster.cs b/ln.ethercat/ECMaster.cs new file mode 100644 index 0000000..4cdccbf --- /dev/null +++ b/ln.ethercat/ECMaster.cs @@ -0,0 +1,379 @@ + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using ln.logging; +using ln.type; + +namespace ln.ethercat +{ + + public enum ECMasterState { + INITIALIZED, + STARTING, + STOPPING, + RUNNING, + } + + public delegate void ECStateChange(ECMaster sender,ECSlaveState newState); + + public class ECMaster + { + public int TIMEOUT_PREOP = 3000; + public int TIMEOUT_SAFEOP = 10000; + public int TIMEOUT_BACKTO_SAFEOP = 200; + public int INTERVALL_PROCESSDATA = 20; + + + public event ECStateChange OnStateChange; + + 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 => UpdateEthercatState(); + set { + if (value != ethercatState) + { + OnStateChange?.Invoke(this, value); + ethercatState = value; + Logging.Log(LogLevel.DEBUG, "ECMaster: EthercatState is now {0}", ethercatState); + } + } + + } + + + Dictionary pdoMap = new Dictionary(); + SDOCache sdoCache; + + 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"); + + sdoCache = new SDOCache(this); + } + + bool stopProcessing; + public bool StopProccessing { + get => stopProcessing; + set => stopProcessing = value; + } + + Thread threadProcessData; + + public bool Start() + { + if (threadProcessData?.IsAlive ?? false) + throw new Exception("already started"); + + EthercatState = ECSlaveState.BOOT; + ExpectedWorkCounter = 0; + + EthercatState = ECSlaveState.INIT; + lock (this) + { + CountSlaves = ECMBind.ecmbind_config_init(); + if (CountSlaves <= 0) + { + Logging.Log(LogLevel.DEBUG, "ECMaster: no slaves connected"); + return false; + } + + if (!CheckState(ECSlaveState.PRE_OP, TIMEOUT_PREOP)) + return false; + + IOMapSize = ECMBind.ecmbind_config_map(); + Logging.Log(LogLevel.DEBUG, "ECMaster: IOMapSize={0}", IOMapSize); + + UpdatePDOMap(); + + threadProcessData = new Thread(MasterThread); + threadProcessData.Start(); + + if (!RequestState(ECSlaveState.SAFE_OP, out ECSlaveState slaveState, TIMEOUT_SAFEOP)) + { + Stop(); + return false; + } + + if (!ReadSDOIndeces()) + { + Logging.Log(LogLevel.WARNING, "ECMaster: could not read SDO indeces"); + } + +/* + if (!RequestState(ECSlaveState.OPERATIONAL, out slaveState, TIMEOUT_SAFEOP)) + { + if (slaveState < ECSlaveState.SAFE_OP) + Stop(); + return false; + } +*/ + } + return true; + } + + public void Stop() + { + if (threadProcessData?.IsAlive ?? false) + { + stopProcessing = true; + ECMBind.ecmbind_request_state(0, ECSlaveState.SAFE_OP, TIMEOUT_BACKTO_SAFEOP); + + threadProcessData.Join(); + threadProcessData = null; + } + } + + + object lockIOMap = new object(); + + void MasterThread() + { + ExpectedWorkCounter = ECMBind.ecmbind_get_expected_wkc_size(); + + while (!stopProcessing) + { + int wkc; + + lock (lockIOMap) + { + wkc = ECMBind.ecmbind_processdata(); + } + Thread.Sleep(INTERVALL_PROCESSDATA); + +/* + if (wkc != ExpectedWorkCounter) + { + ExpectedWorkCounter = ECMBind.ecmbind_recover(); + if (ExpectedWorkCounter < 0) + break; + } +*/ + + } + } + + public bool ReadSDO(SDOAddr addr,out byte[] data) + { + byte[] buffer = new byte[128]; + int size; + lock (lockIOMap) + size = ECMBind.ecmbind_sdo_read(addr.Slave,addr.Index,addr.SubIndex, buffer, buffer.Length); + + if (size < 0) + { + data = new byte[0]; + return false; + } + data = buffer.Slice(0, size); + return true; + } + public bool WriteSDO(SDOAddr addr,byte[] data) + { + throw new NotImplementedException(); + } + + public ECSlaveState ReadSlaveState(int slave) => ECMBind.ecmbind_get_slave_state(slave); + + private ECSlaveState UpdateEthercatState() + { + EthercatState = ECMBind.ecmbind_read_state(); + return ethercatState; + } + + public bool CheckState(ECSlaveState minimumState, int timeout) + { + for (; timeout > 0; timeout -= 10) + { + ECSlaveState state = ECMBind.ecmbind_read_state(); + if (state >= minimumState) + { + EthercatState = state; + return true; + } + Thread.Sleep(10); + } + return false; + } + public bool RequestState(ECSlaveState requestedState, out ECSlaveState reachedState, int timeout) + { + reachedState = ECMBind.ecmbind_request_state(0, requestedState, timeout); + Logging.Log(LogLevel.DEBUG, "ECMaster.RequestState({1}): lowest slave state: {0}", reachedState, requestedState); + EthercatState = reachedState; + return (reachedState >= requestedState); + } + + public int RequestSlaveState(int slave, ECSlaveState slaveState) => ECMBind.ecmbind_write_slave_state(slave, slaveState); + + public bool GetSDO(SDOAddr address,out SDO sdo) + { + sdo = sdoCache.GetOrCreateDescriptor(address); + return true; + } + + public IEnumerable GetSDOs(int slave) => sdoCache.GetSlaveCache(slave).Values; + + +/* public bool ReadSDOSubDescriptors(SDODescriptor descriptor) + { + bool success = true; + + for (int sub = 1; sub < descriptor.SubDescriptors.Length; sub++) + { + if (IOLocked(()=>ECMBind.ecmbind_read_objectdescription_entry((ushort)descriptor.Slave, (ushort)descriptor.Index, (ushort)sub, (int slave, int index, ECDataTypes dataType, ECObjectCodes objectCode, int current_sub, String name) => { + //Logging.Log(LogLevel.DEBUG,"SDO Entry --> {0} {1} {2} {3} {4} {5}",slave,index, dataType, objectCode, current_sub, name); + + descriptor.SubDescriptors[current_sub].DataType = dataType; + descriptor.SubDescriptors[current_sub].Name = name; + })) <= 0) + { + //Logging.Log(LogLevel.WARNING,"ECMaster: ReadSDODescriptors({0}: failed to read descriptor for 0x{1:x8}.{2}", descriptor.Slave, descriptor.Index, sub); + success = false; + } + } + return success; + } +*/ + public bool ReadSDOIndeces() + { + for (int slave=1; slave <= CountSlaves; slave++) + { + if (!ReadSDOIndex(slave)) + return false; + } + return true; + } + public bool ReadSDOIndex(int slave) + { + IOLocked(()=>ECMBind.ecmbind_enumerate_servicedescriptors(slave, (int slave, int index) => { + sdoCache.GetOrCreateDescriptor(new SDOAddr(slave, index)); + })); + Logging.Log(LogLevel.DEBUG, "Indexed SDOs of slave {0}", slave); + return true; + } + +/* + public bool ReadSDODescriptor(SDODescriptor descriptor) + { + IOLocked(()=>ECMBind.ecmbind_enumerate_servicedescriptors(slave, (int slave, int index) => { + descriptors.Add(new SDODescriptor(){ + Slave = slave, + Index = index + }); + })); + + } + + public SDODescriptor[] ReadSDODescriptors(int slave) + { + List descriptors = new List(); + + IOLocked(()=>ECMBind.ecmbind_enumerate_servicedescriptors(slave, (int slave, int index) => { + descriptors.Add(new SDODescriptor(){ + Slave = slave, + Index = index + }); + })); + + foreach (SDODescriptor descriptor in descriptors) + { + if (IOLocked(()=>ECMBind.ecmbind_read_objectdescription(descriptor.Slave, descriptor.Index, (int slave, int index, ECDataTypes dataType, ECObjectCodes objectCode, int maxsub, String name) => { + descriptor.MaxSubindex = maxsub; + descriptor.ObjectCode = objectCode; + descriptor.CreateAndInitializeSubDescriptors(maxsub+1); + descriptor.SubDescriptors[0].DataType = dataType; + descriptor.SubDescriptors[0].Name = name ?? ""; + + //Logging.Log(LogLevel.DEBUG, "SDODescriptor: {0}", descriptor); + //Logging.Log(LogLevel.DEBUG,"SDO --> {0} {1} {2} {3} {4} {5}",slave,index, dataType,objectCode, maxsub, name); + + switch (descriptor.ObjectCode) + { + case ECObjectCodes.VAR: + break; + default: + ReadSDOSubDescriptors(descriptor); + break; + } + })) <= 0) + { + Logging.Log(LogLevel.WARNING,"ECMaster: ReadSDODescriptors({0}: failed to read descriptor for 0x{1:x8}", slave, descriptor.Index); + } + + } + + return descriptors.ToArray(); + } +*/ + + public void UpdatePDOMap() + { + List pdoList = new List(); + 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 (this) + { + IOMapPtr = ECMBind.ecmbind_get_iomap(); + pdoMap.Clear(); + + foreach (PDO pdo in pdoList) + pdoMap.Add(pdo.Address, pdo); + } + + } + + public PDO[] GetPDOMap() => pdoMap.Values.ToArray(); + public bool TryGetPDO(SDOAddr address, out PDO pdo) => pdoMap.TryGetValue(address, out pdo); + + +/* + public unsafe bool IOMapExtract(int offset,int size, byte[] buffer) + { + if (buffer.Length < size) + throw new ArgumentOutOfRangeException(nameof(buffer)); + + byte* iomap = (byte*)(IOMapPtr.ToPointer()) + offset; + fixed (byte* b = buffer) + { + Buffer.MemoryCopy(iomap, b, size, size); + } + + return true; + } +*/ + + T IOLocked(Func f) { + lock (lockIOMap) + return f(); + } + } +} \ No newline at end of file diff --git a/ln.ethercat/ECObjectCodes.cs b/ln.ethercat/ECObjectCodes.cs new file mode 100644 index 0000000..4549413 --- /dev/null +++ b/ln.ethercat/ECObjectCodes.cs @@ -0,0 +1,18 @@ + + +using System; + +namespace ln.ethercat +{ + public enum ECObjectCodes : UInt16 + { + NULL = 0, + DOMAIN = 2, + DEFTYPE = 5, + DEFSTRUCT = 6, + VAR = 7, + ARRAY = 8, + RECORD = 9, + ENUMDefinition = 40 + } +} \ No newline at end of file diff --git a/ln.ethercat/ECSlaveState.cs b/ln.ethercat/ECSlaveState.cs new file mode 100644 index 0000000..9b3060f --- /dev/null +++ b/ln.ethercat/ECSlaveState.cs @@ -0,0 +1,18 @@ + +using System; + +namespace ln.ethercat +{ + [Flags] + public enum ECSlaveState : UInt16 + { + NONE = 0, + INIT = 1, + PRE_OP = 2, + BOOT = 3, + SAFE_OP = 4, + OPERATIONAL = 8, + ERROR = 16, + ACK = 16 + } +} \ No newline at end of file diff --git a/ln.ethercat/PDO.cs b/ln.ethercat/PDO.cs new file mode 100644 index 0000000..b90935c --- /dev/null +++ b/ln.ethercat/PDO.cs @@ -0,0 +1,32 @@ + +using System; +using System.ComponentModel.DataAnnotations; + +namespace ln.ethercat +{ + public class PDO + { + readonly ECMaster ECMaster; + + public SDO SDO { get; } + public SDOAddr Address => SDO.Address; + + public int AddressOffset { get; set; } + public int AddressBit { get; set; } + public int BitLength { get; set; } + public int ByteLength { + get => (BitLength + 7) / 8; + set => BitLength = value * 8; + } + + public PDO(ECMaster ecMaster, UInt16 slave, UInt16 index, byte subIndex) + { + ECMaster = ecMaster; + ECMaster.GetSDO(new SDOAddr(slave, index, subIndex), out SDO sdo); + SDO = sdo; + } + + + + } +} \ No newline at end of file diff --git a/ln.ethercat/SDO.cs b/ln.ethercat/SDO.cs new file mode 100644 index 0000000..fb8f78c --- /dev/null +++ b/ln.ethercat/SDO.cs @@ -0,0 +1,92 @@ + + +using System; +using ln.json.mapping; +using ln.logging; +using ln.type; + +namespace ln.ethercat +{ + public class SDO + { + + public static SDO Create(ECMaster ecMaster, SDOAddr address) + { + SDO sdo = new SDO(ecMaster, address); + if (address.SubIndex == 0) + { + if (ECMBind.ecmbind_read_objectdescription(address.Slave, address.Index, (int slave, int index, ECDataTypes dataType, ECObjectCodes objectCode, int maxsub, String name)=>{ + sdo.DataType = dataType; + sdo.ObjectCode = objectCode; + sdo.MaxSubIndex = maxsub; + sdo.Name = name; + }) <= 0) + { + Logging.Log(LogLevel.WARNING, "cannot create SDO instance for {0}. not found.", address); + return null; + } + } else { + if (ECMBind.ecmbind_read_objectdescription_entry((ushort)address.Slave, (ushort)address.Index, (ushort)address.SubIndex, (int slave, int index, ECDataTypes dataType, ECObjectCodes objectCode, int maxsub, String name)=>{ + sdo.DataType = dataType; + sdo.MaxSubIndex = -1; + sdo.Name = name; + }) <= 0) + { + Logging.Log(LogLevel.WARNING, "cannot create SDO instance for {0}. not found.", address); + return null; + } + } + return sdo; + } + + public SDOAddr Address { get; } + public string Name { get; private set; } + + public ECDataTypes DataType { get; set; } + public ECObjectCodes ObjectCode { get; set; } + public int MaxSubIndex { get; set; } + + public bool IsPartOfPDO => ECMaster.TryGetPDO(Address, out PDO pdo); + + byte[] rawData; + public byte[] RawData { + get { + if (ECMaster.TryGetPDO(Address, out PDO pdo)) + { + if ((rawData == null) || (rawData.Length != pdo.ByteLength)) + rawData = new byte[pdo.ByteLength]; + + ECMBind.ecmbind_iomap_get(pdo.AddressOffset, rawData, pdo.ByteLength); + } else { + ECMaster.ReadSDO(Address, out rawData); + } + return rawData; + } + set => rawData = value; + } + public object Value { + get => ECDataTypeConverter.ConvertFromEthercat(DataType ,RawData); + set => throw new NotImplementedException(); + } + + ECMaster ECMaster { get; } + + SDO(ECMaster ecMaster, SDOAddr address) + { + ECMaster = ecMaster; + Address = address; + } + + public override string ToString() + { + return string.Format("[SDO Slave={0} Index={1:X4}.{7} MaxSubindex={2} ObjectCode={5} DataType={3} Name={4} RawData={6}]", Address.Slave, Address.Index, MaxSubIndex, DataType, Name, ObjectCode, RawData?.ToHexString(), Address.SubIndex); + } + + public override bool Equals(object obj) => (obj is SDO other) && Address.Equals(other.Address); + public override int GetHashCode() => Address.GetHashCode(); + + public T GetValue() => Cast.To(Value); + + } + +} \ No newline at end of file diff --git a/ln.ethercat/SDOAddr.cs b/ln.ethercat/SDOAddr.cs new file mode 100644 index 0000000..9b79b06 --- /dev/null +++ b/ln.ethercat/SDOAddr.cs @@ -0,0 +1,35 @@ + + +namespace ln.ethercat +{ + public class SDOAddr + { + public int Slave { get; private set; } + public int Index { get; private set; } + public int SubIndex { get; private set; } + + public long Linear { get; private set; } + public int Compact { get; private set; } + + public SDOAddr(int slave, int index) : this(slave, index, 0) { } + public SDOAddr(int slave, int index, int subindex) + { + Slave = slave; + Index = index; + SubIndex = subindex; + + Linear = ((long)(ulong)Slave << 32) | ((long)Index << 16) | (long)SubIndex; + Compact = ((Slave << 24) | (Index << 8) | SubIndex) ^ ((Slave & 0xFF00) << 24); + } + + public override bool Equals(object obj) => (obj is SDOAddr other) && (Linear == other.Linear); + public override int GetHashCode() => Compact; + + public override string ToString() + { + return string.Format("[SDOAddr Slave={0} Index={1:X4}.{2} ]", Slave, Index, SubIndex); + } + + + } +} \ No newline at end of file diff --git a/ln.ethercat/SDOCache.cs b/ln.ethercat/SDOCache.cs new file mode 100644 index 0000000..d491877 --- /dev/null +++ b/ln.ethercat/SDOCache.cs @@ -0,0 +1,48 @@ + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using ln.collections; + +namespace ln.ethercat +{ + public class SDOCache + { + ECMaster ECMaster { get; } + + Dictionary> slaveCaches = new Dictionary>(); + + public SDOCache(ECMaster ecMaster) + { + ECMaster = ecMaster; + } + + public BTree GetSlaveCache(int slave) + { + if (!slaveCaches.TryGetValue(slave, out BTree slaveCache)) + { + slaveCache = new BTree(); + slaveCaches.Add(slave, slaveCache); + } + return slaveCache; + } + + + public SDO GetOrCreateDescriptor(SDOAddr address) + { + BTree slaveCache = GetSlaveCache(address.Slave); + + if (!slaveCache.TryGet(address.Linear, out SDO sdo)) + { + sdo = SDO.Create(ECMaster, address); + slaveCache.Add(sdo.Address.Linear, sdo); + } + return sdo; + } + + public void Clear() => slaveCaches.Clear(); + public bool Contains(SDOAddr addr) => GetSlaveCache(addr.Slave).ContainsKey(addr); + public SDO this[SDOAddr address] => GetSlaveCache(address.Slave)[address.Linear]; + + } +} \ No newline at end of file diff --git a/ln.ethercat/controller/ControlLoop.cs b/ln.ethercat/controller/ControlLoop.cs new file mode 100644 index 0000000..a703972 --- /dev/null +++ b/ln.ethercat/controller/ControlLoop.cs @@ -0,0 +1,92 @@ + + +using System; + +namespace ln.ethercat.controller +{ + public delegate double LoopGetProcessValueDelegate(); + + public abstract class ControlLoop + { + public Controller Controller { get; } + + public LoopGetProcessValueDelegate GetProcessValueDelegate { get; set; } + + public double SetPoint { get; set; } + public double ErrorValue { get; private set; } + public double Output { get; private set; } + + public ControlLoop(Controller controller, LoopGetProcessValueDelegate getProcessValueDelegate) + { + Controller = controller; + GetProcessValueDelegate = getProcessValueDelegate; + } + + public virtual void Clear() + { + ErrorValue = 0; + Output = 0; + } + public virtual void Loop() => Loop(GetProcessValueDelegate()); + public virtual void Loop(double processValue) + { + ErrorValue = SetPoint - processValue; + Output = LoopImplementation(); + } + + protected abstract double LoopImplementation(); + protected abstract void ClearImplementation(); + } + + public class PIDControlLoop : ControlLoop + { + public double Kp { get; set; } = 1.0; + public double Ki { get; set; } = 1.0; + public double Tn { + get => 1.0 / Ki; + set => Ki = (value == 0) ? 0.0 : (1.0 / value); + } + + public double Kd { get; set; } = 1.0; + + + public PIDControlLoop(Controller controller, LoopGetProcessValueDelegate getProcessValueDelegate) + :base(controller, getProcessValueDelegate) + {} + + public double Integral { get; set; } + public double MinIntregal { get; set; } = double.MinValue; + public double MaxIntegral { get; set; } = double.MaxValue; + public double LastError { get; private set; } + + protected override void ClearImplementation() + { + Integral = Math.Clamp(0, MinIntregal, MaxIntegral); + LastError = 0; + } + + protected override double LoopImplementation() + { + double output = (Kp * ErrorValue); + + if (Ki > 0.0) + { + Integral += (ErrorValue * Controller.ControllerLoopInterval * Ki) * Kp; + Integral = Math.Clamp(Integral, MinIntregal, MaxIntegral); + output += Integral; + } + if (Kd != 0) + { + output += (Kd * (ErrorValue - LastError) * Controller.ControllerLoopInterval) * Kp; + LastError = ErrorValue; + } + + return output; + } + + + + } + + +} \ No newline at end of file diff --git a/ln.ethercat/controller/Controller.cs b/ln.ethercat/controller/Controller.cs new file mode 100644 index 0000000..f17e8d3 --- /dev/null +++ b/ln.ethercat/controller/Controller.cs @@ -0,0 +1,101 @@ + +using System.Collections.Generic; +using System.Threading; +using ln.ethercat.controller.drives; +using ln.logging; + +namespace ln.ethercat.controller +{ + + public delegate void ControllerLogicDelegate(Controller controller); + + public class Controller + { + public event ControllerLogicDelegate ControllerLogic; + + + List driveControllers = new List(); + public DriveController[] DriveControllers => driveControllers.ToArray(); + + List controlLoops = new List(); + public ControlLoop[] ControlLoops => controlLoops.ToArray(); + + public bool IsRunning => threadController?.IsAlive ?? false; + + public double ControllerLoopInterval { get; set; } = 0.1; + public double ControllerLoopFrequency { + get => 1.0 / ControllerLoopInterval; + set => ControllerLoopInterval = 1.0 / value; + } + + bool stopRequested; + Thread threadController; + + public Controller() + { + } + + public void Start() + { + if (threadController?.IsAlive ?? false) + { + Logging.Log(LogLevel.WARNING, "Controller; Start(): already started"); + } else { + stopRequested = false; + threadController = new Thread(ControllerThread); + threadController.Start(); + } + } + + public void Stop() + { + if (threadController?.IsAlive ?? false) + { + stopRequested = true; + threadController.Join(); + threadController = null; + } + } + + public void Add(DriveController driveController) => driveControllers.Add(driveController); + public void Remove(DriveController driveController) => driveControllers.Remove(driveController); + + public void Add(ControlLoop controlLoop) => controlLoops.Add(controlLoop); + public void Remove(ControlLoop controlLoop) => controlLoops.Remove(controlLoop); + + public DriveStates DrivesState { + get { + DriveStates lowestState = DriveStates.OPERATIONAL; + foreach (DriveController driveController in driveControllers) + { + DriveStates driveState = driveController.DriveState; + if (driveState < lowestState) + lowestState = driveState; + } + return lowestState; + } + } + + void ControllerThread() + { + while (!stopRequested) + { + foreach (DriveController driveController in driveControllers) + driveController.UpdateStates(); + + foreach (ControlLoop controlLoop in controlLoops) + controlLoop.Loop(); + + ControllerLogic?.Invoke(this); + + foreach (DriveController driveController in driveControllers) + driveController.UpdateDrive(); + + Thread.Sleep((int)(1000.0 * ControllerLoopInterval)); + } + } + + + + } +} \ No newline at end of file diff --git a/ln.ethercat/controller/drives/CIA402Controller.cs b/ln.ethercat/controller/drives/CIA402Controller.cs new file mode 100644 index 0000000..988f588 --- /dev/null +++ b/ln.ethercat/controller/drives/CIA402Controller.cs @@ -0,0 +1,242 @@ + + +using System; +using ln.logging; + +namespace ln.ethercat.controller.drives +{ + public enum CIA402States { + UNDEFINED, + NOT_READY_TO_SWITCH_ON, + SWITCH_ON_DISABLED, + READY_TO_SWITCH_ON, + SWITCHED_ON, + OPERATION_ENABLED, + QUICK_STOP_ACTIVE, + FAULT_REACTION_ACTIVE, + FAULT + } + + public enum CIA402ModesOfOperation : byte + { + NO_MODE_CHANGE = 0, + VL_VELOCITY_MODE = 2, + HOMING_MODE = 6, + INTERPOLATED_POSITION_MODE = 7, + CYCLIC_SYNC_POSITION = 8, + CYCLIC_SYNC_VELOCITY = 9, + CYCLIC_SYNC_TORQUE = 10 + } + + public class CIA402Controller : DriveController + { + public readonly SDOAddr saControlWord; + public readonly SDOAddr saStatusWord; + public readonly SDOAddr saErrorCode; + public readonly SDOAddr saTargetPosition; + public readonly SDOAddr saTargetSpeed; + public readonly SDOAddr saTargetTorque; + public readonly SDOAddr saActualPosition; + public readonly SDOAddr saActualSpeed; + public readonly SDOAddr saActualTorque; + public readonly SDOAddr saModesOfOperation; + public readonly SDOAddr saModesOfOperationDisplay; + + public CIA402Controller(ECMaster ecMaster,int slave) + :base(ecMaster, slave) + { + saErrorCode = new SDOAddr(slave, 0x603f); + saControlWord = new SDOAddr(slave, 0x6040); + saStatusWord = new SDOAddr(slave, 0x6041); + + saTargetPosition = new SDOAddr(slave, 0x607A); + saTargetSpeed = new SDOAddr(slave, 0x60FF); + saTargetTorque = new SDOAddr(slave, 0x6071); + + saActualPosition = new SDOAddr(slave, 0x6064); + saActualSpeed = new SDOAddr(slave, 0x606C); + saActualTorque = new SDOAddr(slave, 0x6077); + + saModesOfOperation = new SDOAddr(slave, 0x6060); + saModesOfOperationDisplay = new SDOAddr(slave, 0x6061); + } + + public override void UpdateStates() { } + public override void UpdateDrive() { } + + public override DriveStates DriveState + { + get { + switch (GetCIA402State()) + { + case CIA402States.NOT_READY_TO_SWITCH_ON: + case CIA402States.SWITCH_ON_DISABLED: + case CIA402States.READY_TO_SWITCH_ON: + return DriveStates.INIT; + case CIA402States.SWITCHED_ON: + case CIA402States.QUICK_STOP_ACTIVE: + return DriveStates.POWERED; + case CIA402States.OPERATION_ENABLED: + return DriveStates.OPERATIONAL; + case CIA402States.FAULT_REACTION_ACTIVE: + case CIA402States.FAULT: + return DriveStates.ERROR; + default: + return DriveStates.UNDEFINED; + } + } + } + + public override string OEMDriveState { + get => GetCIA402State().ToString(); + } + + public override DriveMode DriveMode { + get { + switch (ModeOfOperation) + { + case CIA402ModesOfOperation.CYCLIC_SYNC_POSITION: + return DriveMode.POSITION; + case CIA402ModesOfOperation.CYCLIC_SYNC_VELOCITY: + return DriveMode.SPEED; + case CIA402ModesOfOperation.CYCLIC_SYNC_TORQUE: + return DriveMode.TORQUE; + default: + return DriveMode.UNDEFINED; + } + } + set { + switch (value) + { + case DriveMode.POSITION: + ModeOfOperation = CIA402ModesOfOperation.CYCLIC_SYNC_POSITION; + break; + case DriveMode.SPEED: + ModeOfOperation = CIA402ModesOfOperation.CYCLIC_SYNC_POSITION; + break; + case DriveMode.TORQUE: + ModeOfOperation = CIA402ModesOfOperation.CYCLIC_SYNC_POSITION; + break; + default: + Logging.Log(LogLevel.WARNING, "CIA402Controller: DriveMode {0} not supported", value); + break; + } + } + } + + public CIA402ModesOfOperation ModeOfOperation { + get => (CIA402ModesOfOperation)GetSDOValue(saModesOfOperationDisplay); + set => SetSDOValue(saModesOfOperation, (byte)value); + } + + + public override decimal ActualPosition => GetSDOValue(saActualPosition); + public override decimal ActualSpeed => GetSDOValue(saActualSpeed); + public override decimal ActualTorque => GetSDOValue(saActualTorque); + + public override decimal TargetPosition { + get => GetSDOValue(saTargetPosition); + set => SetSDOValue(saTargetPosition, value); + } + + public override decimal TargetSpeed { + get => GetSDOValue(saTargetSpeed); + set => SetSDOValue(saTargetSpeed, value); + } + public override decimal TargetTorque { + get => GetSDOValue(saTargetTorque); + set => SetSDOValue(saTargetTorque, value); + } + + public override int ErrorCode => GetSDOValue(saErrorCode); + + public override string ErrorText => ErrorCode.ToString(); + + public override void EnableDrive(bool enable) + { + if (enable) + { + switch (CIA402State) + { + case CIA402States.SWITCHED_ON: + SetSDOValue(saControlWord, (UInt16)0x000F); + break; + default: + Logging.Log(LogLevel.WARNING, "CIA402Controller: EnableDrive(): current state is {0}, can't enable drive", CIA402State.ToString()); + break; + } + } else { + switch (CIA402State) + { + case CIA402States.OPERATION_ENABLED: + SetSDOValue(saControlWord, (UInt16)0x0007); + break; + default: + Logging.Log(LogLevel.WARNING, "CIA402Controller: EnableDrive(): current state is {0}, can't disable drive", CIA402State.ToString()); + break; + } + } + } + + public override void Power(bool poweron) + { + if (poweron) + { + switch (CIA402State) + { + case CIA402States.FAULT: + case CIA402States.FAULT_REACTION_ACTIVE: + Logging.Log(LogLevel.WARNING, "CIA402Controller: Power(): Drive in fault state, not ready to switch power on"); + break; + case CIA402States.NOT_READY_TO_SWITCH_ON: + Logging.Log(LogLevel.WARNING, "CIA402Controller: Power(): Drive not ready to switch power on"); + break; + case CIA402States.SWITCH_ON_DISABLED: + case CIA402States.READY_TO_SWITCH_ON: + SetSDOValue(saControlWord, (UInt16)0x0007); // Switch ON + break; + } + } else { + SetSDOValue(saControlWord, (UInt16)0x0006); + } + } + + public override void ClearFault() + { + switch (CIA402State) + { + case CIA402States.FAULT: + SetSDOValue(saControlWord, (UInt16)0x0086); + break; + } + } + + CIA402States CIA402State => GetCIA402State(); + public CIA402States GetCIA402State() + { + if (ECMaster.GetSDO(saStatusWord, out SDO sdoStatusWord)) + { + UInt16 statusword = sdoStatusWord.GetValue(); + + if ((statusword & 0x004F)==0) + return CIA402States.NOT_READY_TO_SWITCH_ON; + if ((statusword & 0x004F)==0x0040) + return CIA402States.SWITCH_ON_DISABLED; + if ((statusword & 0x006F)==0x0021) + return CIA402States.READY_TO_SWITCH_ON; + if ((statusword & 0x006F)==0x0023) + return CIA402States.SWITCHED_ON; + if ((statusword & 0x006F)==0x0027) + return CIA402States.OPERATION_ENABLED; + if ((statusword & 0x006F)==0x0007) + return CIA402States.QUICK_STOP_ACTIVE; + if ((statusword & 0x004F)==0x000F) + return CIA402States.FAULT_REACTION_ACTIVE; + if ((statusword & 0x004F)==0x0008) + return CIA402States.FAULT; + } + return CIA402States.UNDEFINED; + } + + } +} \ No newline at end of file diff --git a/ln.ethercat/controller/drives/DriveController.cs b/ln.ethercat/controller/drives/DriveController.cs new file mode 100644 index 0000000..164d01f --- /dev/null +++ b/ln.ethercat/controller/drives/DriveController.cs @@ -0,0 +1,88 @@ + + +using System; +using System.Runtime; + +namespace ln.ethercat.controller.drives +{ + public enum DriveStates { + UNDEFINED, + BOOT, + INIT, + ERROR, + POWERED, + OPERATIONAL, + } + + public enum DriveMode { + UNDEFINED, + TORQUE, + SPEED, + POSITION + } + + public abstract class DriveController + { + protected ECMaster ECMaster { get; } + public int Slave { get; } + + public DriveController(ECMaster ecMaster, int slave) + { + ECMaster = ecMaster; + Slave = slave; + } + + /* called by controller before control loops and logic */ + public abstract void UpdateStates(); + /* called by controller after user logic */ + public abstract void UpdateDrive(); + + public abstract DriveStates DriveState { get; } + public abstract string OEMDriveState { get; } + public abstract Int32 ErrorCode { get; } + public abstract string ErrorText { get; } + + public abstract void ClearFault(); + + public abstract DriveMode DriveMode { get; set; } + + public void PowerOn() => Power(true); + public void PowerOff() => Power(false); + public abstract void Power(bool poweron); + + public void EnableDrive() => EnableDrive(true); + public void DisableDrive() => EnableDrive(false); + public abstract void EnableDrive(bool enabled); + + public abstract decimal ActualPosition { get; } + public abstract decimal ActualSpeed { get; } + public abstract decimal ActualTorque { get; } + public abstract decimal TargetPosition { get; set; } + public abstract decimal TargetSpeed { get; set; } + public abstract decimal TargetTorque { get; set; } + + + public T GetSDOValue(UInt16 index,byte subIndex) + { + if (ECMaster.GetSDO(new SDOAddr(Slave, index, subIndex), out SDO sdo)) + return sdo.GetValue(); + throw new Exception("DriveController: failed to get sdo value"); + } + public T GetSDOValue(SDOAddr sa) + { + if (ECMaster.GetSDO(sa, out SDO sdo)) + return sdo.GetValue(); + throw new Exception("DriveController: failed to get sdo value"); + } + + public void SetSDOValue(UInt16 slave, UInt16 index, byte subIndex, T value) => SetSDOValue(new SDOAddr(slave,index,subIndex), value); + public void SetSDOValue(SDOAddr sa, T value) + { + if (ECMaster.GetSDO(sa, out SDO sdo)) + sdo.Value = value; + } + + + } + +} \ No newline at end of file diff --git a/ln.ethercat/lib/libecmbind.so b/ln.ethercat/lib/libecmbind.so new file mode 100755 index 0000000000000000000000000000000000000000..d49743af8051b12a0096af25db50f7bfa9f3067d GIT binary patch literal 179504 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 z

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

F*D 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 literal 0 HcmV?d00001 diff --git a/ln.ethercat/ln.ethercat.csproj b/ln.ethercat/ln.ethercat.csproj new file mode 100644 index 0000000..205ce7d --- /dev/null +++ b/ln.ethercat/ln.ethercat.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + true + 0.1.0-ci + Harald Wolff-Thobaben + l--n.de + A simple ethercat master based on SOEM (https://openethercatsociety.github.io/) + (c) 2020 Harald Wolff-Thobaben + ethercat master + + + + + + + + + + + + + +