diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 27090aa..332f8da 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -12,10 +12,15 @@ jobs: - uses: actions/checkout@v4 - name: Install clang-format - run: sudo apt-get install -y clang-format + run: | + sudo apt-get update + sudo apt-get install -y clang-format + clang-format --version - name: Check formatting run: | - find code/ extras/ -name "*.cpp" -o -name "*.h" \ + find code/ extras/ \ + -path "extras/oled_status/u8g8-arm" -prune -o \ + \( -name "*.cpp" -o -name "*.h" \) -print \ | grep -v "cmake-build" \ - | xargs clang-format --dry-run --Werror + | xargs clang-format-18 --dry-run --Werror \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cfc71c8..f1060c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,17 +164,13 @@ add_executable(led_demo code/led_demo.cpp code/gpios.cpp) set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS}") target_link_libraries(led_demo gpiod pthread) -add_executable(fake_pps code/fake_pps.cpp code/gpios.cpp) -set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS}") -target_link_libraries(fake_pps ${LIBSERIAL_LIBRARY} gpiod pthread) - add_executable(button_demo code/button_demo.cpp code/gpios.cpp) set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS}") target_link_libraries(button_demo gpiod pthread) # Define install directories -install(TARGETS control_program led_demo fake_pps button_demo +install(TARGETS control_program led_demo button_demo RUNTIME DESTINATION /opt/mandeye/) set(MANDEYE_USE_LIBCAMERA ON CACHE BOOL "Build libcamera extra") @@ -184,13 +180,18 @@ if(MANDEYE_USE_LIBCAMERA) add_subdirectory(extras/libcamera) endif() add_subdirectory(extras/rtkNmea) +set(MANDEYE_BUILD_EXTRAS_MURATA_UART_IMU ON CACHE BOOL "Build Murata UART IMU extra module - Experimental") +if (MANDEYE_BUILD_EXTRAS_MURATA_UART_IMU) + message(STATUS "Building Murata SPI IMU extra module") + add_subdirectory(extras/MurataUartImu) +endif() + add_subdirectory(extras/FakePPS) add_subdirectory(extras/SlavePPS) add_subdirectory(extras/oled_status) install(FILES packing/helpers.sh DESTINATION /opt/mandeye/) install(FILES packing/services/mandeye_controller.service DESTINATION /usr/lib/systemd/system) -install(FILES packing/services/mandeye_fake_pps.service DESTINATION /usr/lib/systemd/system) install(FILES packing/services/mandeye_phc2sys-gm-eth0.service DESTINATION /usr/lib/systemd/system) install(FILES packing/services/mandeye_ptp4l-gm-eth0.service DESTINATION /usr/lib/systemd/system) @@ -203,4 +204,4 @@ set(CPACK_PACKAGE_VERSION "${MANDEYE_VERSION}-${GIT_HASH}-${MANDEYE_HARDWARE_HEA set(CPACK_PACKAGE_CONTACT "Michał Pełka ") set(CPACK_DEBIAN_PACKAGE_DEPENDS "rapidjson-dev,libserial-dev, libgpiod-dev, libzmq3-dev") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/packing/postinst/postinst") -include(CPack) \ No newline at end of file +include(CPack) diff --git a/README.md b/README.md index 4c14397..9a57dd2 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,11 @@ sudo systemctl enable mandeye_extra_gnss.service mandeye_extra_gnss_start ``` +Extra muRata UART IMU (experimental): +```shell +sudo systemctl enable mandeye_murata_uart_driver.service +mandeye_murata_uart_imu_start +``` # Services List state of services: ```shell diff --git a/code/fake_pps.cpp b/code/fake_pps.cpp index 1700db2..e69de29 100644 --- a/code/fake_pps.cpp +++ b/code/fake_pps.cpp @@ -1,176 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "hardware_config/mandeye.h" - -namespace NMEA -{ -const unsigned int BufferLen = 128; - -//! Helper structure to get time -struct timestamp -{ - uint8_t hours; - uint8_t mins; - uint8_t secs; - uint8_t day; - uint8_t month; - uint8_t year; -}; - -std::string produceNMEA(const NMEA::timestamp& ts) -{ - char buffer[BufferLen]; - char payload[BufferLen]; - snprintf( - // payload, NMEA::BufferLen, "GPRMC,%02d%02d%02d.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,%s,0.0,E,D", ts.hours, ts.mins, ts.secs, date); - payload, - NMEA::BufferLen, - "GPRMC,%02d%02d%02d.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,%02d%02d%02d,0.0,E,D", - ts.hours, - ts.mins, - ts.secs, - ts.day, - ts.month, - ts.year); - size_t len = strnlen(payload, NMEA::BufferLen); - // compute NMEA checksum on buffer - uint8_t NMEAChecksumComputed = 0; - - size_t i = 0; - for(i = 0; i < len; i++) - { - NMEAChecksumComputed ^= payload[i]; - } - // attach cheksum - snprintf(buffer, NMEA::BufferLen, "$%s*%02X\n", payload, NMEAChecksumComputed); - return std::string(buffer); -} - -NMEA::timestamp GetTimestampFromSec(time_t secsElapsed) -{ - std::tm* timeInfo = gmtime(&secsElapsed); - NMEA::timestamp ts; - ts.hours = timeInfo->tm_hour; - ts.mins = timeInfo->tm_min; - ts.secs = timeInfo->tm_sec; - ts.day = timeInfo->tm_mday; - ts.month = timeInfo->tm_mon + 1; - ts.year = timeInfo->tm_year - 100; - return ts; -} -} // namespace NMEA - -std::atomic stop{false}; -void oneSecondThread() -{ - // setup serial port - std::vector> serialPorts; - std::vector syncOutsLines; - - const auto portsNames = hardware::GetLidarSyncPorts(); - for(const auto& portName : portsNames) - { - std::cout << "opening port " << portName << std::endl; - std::unique_ptr serialPort = std::make_unique(); - serialPort->Open(portName, std::ios_base::out); - serialPort->SetBaudRate(LibSerial::BaudRate::BAUD_9600); - serialPorts.emplace_back(std::move(serialPort)); - } - const auto ouputs = hardware::GetLidarSyncGPIO(); - const auto& chipPath = hardware::GetGPIOChip(); - std::cout << "Opening GPIO chip " << chipPath << std::endl; - - gpiod_chip* chip = gpiod_chip_open(chipPath); - if(chip == nullptr) - { - std::cerr << "Error: Unable to open GPIO chip." << std::endl; - std::abort(); - } - - for(const auto& pin : ouputs) - { - auto line = gpiod_chip_get_line(chip, pin); - if(line == nullptr) - { - std::cerr << "Error: Unable to open GPIO line." << std::endl; - gpiod_chip_close(chip); - std::abort(); - } - int ret = gpiod_line_request_output(line, "mandeye_fake_pps", 0); - if(ret < 0) - { - std::cerr << "Error: Unable to request GPIO line." << std::endl; - gpiod_chip_close(chip); - std::abort(); - } - syncOutsLines.emplace_back(line); - } - assert(serialPorts.size() == syncOutsLines.size()); - - //setup pps gpio - constexpr uint64_t Rate = 1000; - const auto now = std::chrono::system_clock::now(); - uint64_t millisFromEpoch = std::chrono::duration_cast(now.time_since_epoch()).count(); - millisFromEpoch += Rate; - - //round to next second - millisFromEpoch = (millisFromEpoch / Rate) * Rate; - auto waKeUpTime = std::chrono::system_clock::time_point(std::chrono::milliseconds(millisFromEpoch)); - - while(!stop) - { - std::this_thread::sleep_until(waKeUpTime); - auto currentTime = std::chrono::system_clock::now(); - millisFromEpoch += Rate; - - waKeUpTime = std::chrono::system_clock::time_point(std::chrono::milliseconds(millisFromEpoch)); - - const uint64_t secs = millisFromEpoch / 1000; - NMEA::timestamp ts = NMEA::GetTimestampFromSec(secs); - - for(auto& syncOut : syncOutsLines) - { - gpiod_line_set_value(syncOut, 0); - } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - for(auto& syncOut : syncOutsLines) - { - gpiod_line_set_value(syncOut, 1); - } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - const std::string nmeaMessage = NMEA::produceNMEA(ts); - for(auto& serialPort : serialPorts) - { - serialPort->Write(nmeaMessage); - } - - std::this_thread::sleep_until(waKeUpTime); - } - for(auto& syncOut : syncOutsLines) - { - gpiod_line_release(syncOut); - } - gpiod_chip_close(chip); -} -int main(int arc, char* argv[]) -{ - std::cout << "fake pps" << std::endl; - - std::thread t1(oneSecondThread); - while(!stop) - { - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - } - t1.join(); - return 0; -} diff --git a/code/hardware_config/mandeye-standard-rpi5.h b/code/hardware_config/mandeye-standard-rpi5.h index 366dcbc..8c11176 100644 --- a/code/hardware_config/mandeye-standard-rpi5.h +++ b/code/hardware_config/mandeye-standard-rpi5.h @@ -116,7 +116,7 @@ constexpr GPIO::GPIO_PULL GetPULL([[maybe_unused]] BUTTON btn) [[maybe_unused]] inline const std::string GetGNSSPort() { - return "/dev/ttyAMA0"; + return ""; }; [[maybe_unused]] inline const LibSerial::BaudRate GetGNSSBaudrate() diff --git a/code/lidars/hesai/HesaiClient.cpp b/code/lidars/hesai/HesaiClient.cpp index d727cb3..6cdc6b5 100644 --- a/code/lidars/hesai/HesaiClient.cpp +++ b/code/lidars/hesai/HesaiClient.cpp @@ -170,8 +170,7 @@ void HesaiClient::CallbackFrame(const LidarDecodedFrame& data data.z = point.z; data.intensity = point.intensity; data.laser_id = 0; - data.timestamp = point.timestamp; - m_timestamp = point.timestamp; + data.timestamp = point.timestamp * 1e9; // convert to nanosecs m_bufferLidarPtr->push_back(data); } } @@ -188,13 +187,13 @@ void HesaiClient::CallbackIMU(const LidarImuData& dataFrame) auto millis = std::chrono::duration_cast(duration); LidarIMU data; - data.timestamp = dataFrame.timestamp; + data.timestamp = dataFrame.timestamp * 1e9; data.acc_x = dataFrame.imu_accel_x; data.acc_y = dataFrame.imu_accel_y; data.acc_z = dataFrame.imu_accel_z; - data.gyro_x = dataFrame.imu_ang_vel_x; - data.gyro_y = dataFrame.imu_ang_vel_y; - data.gyro_z = dataFrame.imu_ang_vel_z; + data.gyro_x = M_PI * dataFrame.imu_ang_vel_x / 180.0; // to rads + data.gyro_y = M_PI * dataFrame.imu_ang_vel_y / 180.0; + data.gyro_z = M_PI * dataFrame.imu_ang_vel_z / 180.0; data.laser_id = 0; data.epoch_time = millis.count(); m_bufferIMUPtr->push_back(data); diff --git a/code/lidars/livoxsdk2/LivoxClient.cpp b/code/lidars/livoxsdk2/LivoxClient.cpp index 41ea3b4..81f9411 100644 --- a/code/lidars/livoxsdk2/LivoxClient.cpp +++ b/code/lidars/livoxsdk2/LivoxClient.cpp @@ -97,7 +97,7 @@ nlohmann::json LivoxClient::produceStatus() data["LivoxLidarInfo"]["m_sessionStart_s"] = double(m_sessionStart.value_or(-1.f)) / 1e9; data["LivoxLidarInfo"]["m_elapsed"] = m_elapsed; data["LivoxLidarInfo"]["m_elapsed_s"] = double(m_elapsed) / 1e9; - + data["LivoxLidarInfo"]["m_time_diff"] = m_time_diff; auto arrayworkMode = nlohmann::json::array(); for(auto& mode : m_LivoxLidarWorkMode) { @@ -184,8 +184,9 @@ std::pair LivoxClient::retrieveData() } void LivoxClient::testThread() { - std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "Livox periodical watch thread" << std::endl; + while(!isDone) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); @@ -247,6 +248,11 @@ void LivoxClient::saveTimeStamp(LivoxClient* client, uint64_t timestamp) { client->m_elapsed = client->m_timestamp - client->m_sessionStart.value(); } + using namespace std::chrono; + const auto now = system_clock::now(); + auto duration = now.time_since_epoch(); + double tp = std::chrono::duration(duration).count(); + client->m_time_diff = std::abs(tp - double(client->m_timestamp) / 1e9); } void LivoxClient::PointCloudCallback(uint32_t handle, const uint8_t dev_type, LivoxLidarEthernetPacket* data, void* client_data) @@ -324,7 +330,6 @@ bool LivoxClient::isSynced() return false; } } - std::cout << "Time sync is synced" << std::endl; return true; } @@ -339,6 +344,8 @@ void LivoxClient::ImuDataCallback(uint32_t handle, const uint8_t dev_type, Livox auto millis = std::chrono::duration_cast(duration); LivoxClient* this_ptr = (LivoxClient*)client_data; + // std::cout << "m_time_diff " <m_time_diff << std::endl; + if(data->data_type == kLivoxLidarImuData) { const auto laser_id = this_ptr->handleToLidarId(handle); diff --git a/code/lidars/livoxsdk2/LivoxClient.h b/code/lidars/livoxsdk2/LivoxClient.h index 89d5600..6883543 100644 --- a/code/lidars/livoxsdk2/LivoxClient.h +++ b/code/lidars/livoxsdk2/LivoxClient.h @@ -67,7 +67,7 @@ class LivoxClient : public BaseLidarClient std::unordered_map m_handleToLastTimestamp; std::unordered_map m_handleToSerialNumber; - + double m_time_diff; //! This is a set of serial numbers that we have already seen, its used to find lidarId std::set m_serialNumbers; diff --git a/extras/FakePPS/main.cpp b/extras/FakePPS/main.cpp index 01a09ea..0da294f 100644 --- a/extras/FakePPS/main.cpp +++ b/extras/FakePPS/main.cpp @@ -1,18 +1,54 @@ +#include +#include #include #include +#include +#include #include +#include #include -#include #include #include #include #include -#include -#include -#include -#include -#include +#include "hardware_config/mandeye.h" + +struct JitterStats +{ + long long minUs = std::numeric_limits::max(); + long long maxUs = std::numeric_limits::min(); + double sumUs = 0.0; + double sumSqUs = 0.0; + uint64_t count = 0; + + void update(long long us) + { + minUs = std::min(minUs, us); + maxUs = std::max(maxUs, us); + sumUs += us; + sumSqUs += static_cast(us) * us; + ++count; + } + + void report(long long lastUs) const + { + if(count == 0) + return; + double mean = sumUs / count; + double variance = (sumSqUs / count) - (mean * mean); + double stddev = variance > 0.0 ? std::sqrt(variance) : 0.0; + printf("PPS jitter [us] last=%+lld min=%lld max=%lld mean=%.1f stddev=%.1f n=%llu\n", + lastUs, + minUs, + maxUs, + mean, + stddev, + (unsigned long long)count); + fflush(stdout); + } +}; + namespace NMEA { const unsigned int BufferLen = 128; @@ -36,7 +72,7 @@ std::string produceNMEA(const NMEA::timestamp& ts) // payload, NMEA::BufferLen, "GPRMC,%02d%02d%02d.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,%s,0.0,E,D", ts.hours, ts.mins, ts.secs, date); payload, NMEA::BufferLen, - "GPRMC,%02d%02d%02d.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,%02d%02d%02d,0.0,E,D", + "GPRMC,%02d%02d%02d.00,A,5109.038,N,11401.000,W,000.0,000.0,%02d%02d%02d,000.0,W", ts.hours, ts.mins, ts.secs, @@ -53,7 +89,7 @@ std::string produceNMEA(const NMEA::timestamp& ts) NMEAChecksumComputed ^= payload[i]; } // attach cheksum - snprintf(buffer, NMEA::BufferLen, "$%s*%02X\n", payload, NMEAChecksumComputed); + snprintf(buffer, NMEA::BufferLen, "$%s*%02X\r\n", payload, NMEAChecksumComputed); return std::string(buffer); } @@ -118,56 +154,66 @@ void oneSecondThread() } assert(serialPorts.size() == syncOutsLines.size()); - // Set realtime scheduling (SCHED_FIFO) to minimise wakeup jitter - { - struct sched_param sp - { }; - sp.sched_priority = 80; - if(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0) - std::cerr << "Warning: failed to set SCHED_FIFO (run as root?)" << std::endl; - } - - // Lock all memory pages to prevent page-fault latency - if(mlockall(MCL_CURRENT | MCL_FUTURE) != 0) - std::cerr << "Warning: mlockall failed" << std::endl; - - // Round up to the next whole second boundary - struct timespec wakeup - { }; - clock_gettime(CLOCK_REALTIME, &wakeup); - wakeup.tv_sec += 1; - wakeup.tv_nsec = 0; - - constexpr long PulsWidthNs = 100'000'000L; // 100 ms pulse width + constexpr uint64_t Rate = 1000; + JitterStats jitter; while(!stop) { - // Sleep until exact second boundary - clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &wakeup, nullptr); - - // Rising edge AT the second boundary - for(auto& syncOut : syncOutsLines) - gpiod_line_set_value(syncOut, 1); - - // Falling edge after pulse width - struct timespec pulseEnd = wakeup; - pulseEnd.tv_nsec += PulsWidthNs; - if(pulseEnd.tv_nsec >= 1'000'000'000L) + auto currentTime = std::chrono::system_clock::now(); + + // get deadline for next second + const auto millisFromEpoch = std::chrono::duration_cast(currentTime.time_since_epoch()).count(); + const auto nextMillisFromEpoch = ((millisFromEpoch / Rate) + 1) * Rate; + + const auto waKeUpTime = std::chrono::system_clock::time_point(std::chrono::milliseconds(nextMillisFromEpoch)); + // Sleep until spinMarginUs before the deadline, then busy-spin for precision. + // Spin margin must exceed worst-case sleep overrun (~15us observed). + constexpr int64_t spinMarginUs = 200; + std::this_thread::sleep_until(waKeUpTime - std::chrono::microseconds(spinMarginUs)); + while(std::chrono::system_clock::now() < waKeUpTime) { - pulseEnd.tv_sec += 1; - pulseEnd.tv_nsec -= 1'000'000'000L; + asm volatile("" ::: "memory"); // prevent loop from being optimized away } - clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &pulseEnd, nullptr); - for(auto& syncOut : syncOutsLines) - gpiod_line_set_value(syncOut, 0); - // NMEA sent after the pulse - const std::string nmeaMessage = NMEA::produceNMEA(NMEA::GetTimestampFromSec(wakeup.tv_sec)); - for(auto& serialPort : serialPorts) - serialPort->Write(nmeaMessage); - - // Advance to next second - wakeup.tv_sec += 1; + const auto actualTime = std::chrono::system_clock::now(); + const auto jitterUs = std::chrono::duration_cast(actualTime - waKeUpTime).count(); + if(jitterUs > 10 || jitterUs < -10) + { + std::cerr << "Warning: PPS jitter exceeded 10us: " << jitterUs << "us" << std::endl; + continue; // skip this pulse if jitter is too high + } + else + { + std::cout << "PPS pulse generated with jitter: " << jitterUs << "us" << std::endl; + for(auto& syncOut : syncOutsLines) + { + gpiod_line_set_value(syncOut, 1); + } + + jitter.update(jitterUs); + jitter.report(jitterUs); + + const uint64_t secs = nextMillisFromEpoch / 1000; + NMEA::timestamp ts = NMEA::GetTimestampFromSec(secs); + + auto t = std::thread([&serialPorts, ts]() { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + const std::string nmeaMessage = NMEA::produceNMEA(ts); + for(auto& serialPort : serialPorts) + { + serialPort->Write(nmeaMessage); + } + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + for(auto& syncOut : syncOutsLines) + { + gpiod_line_set_value(syncOut, 0); + } + + t.join(); + } } for(auto& syncOut : syncOutsLines) { @@ -177,6 +223,20 @@ void oneSecondThread() } int main(int arc, char* argv[]) { + // Set realtime scheduling + struct sched_param param; + param.sched_priority = 99; // highest priority + if(sched_setscheduler(0, SCHED_FIFO, ¶m) < 0) + { + std::cerr << "Failed to set realtime priority, run as root" << std::endl; + } + + // Lock memory to prevent page faults + if(mlockall(MCL_CURRENT | MCL_FUTURE) < 0) + { + std::cerr << "Failed to lock memory" << std::endl; + } + std::cout << "fake pps" << std::endl; std::thread t1(oneSecondThread); diff --git a/extras/MurataUartImu/CMakeLists.txt b/extras/MurataUartImu/CMakeLists.txt new file mode 100644 index 0000000..124b860 --- /dev/null +++ b/extras/MurataUartImu/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.13) +project(mandeye_extra_murata_uart_imu CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(ZMQ REQUIRED libzmq) +pkg_check_modules(SERIAL REQUIRED libserial) + +add_executable(mandeye_murata_uart_driver murataDriver.cpp) +target_include_directories(mandeye_murata_uart_driver PRIVATE ${ZMQ_INCLUDE_DIRS} ${SERIAL_INCLUDE_DIRS}) +target_link_libraries(mandeye_murata_uart_driver ${ZMQ_LIBRARIES} ${SERIAL_LIBRARIES}) + +install(TARGETS mandeye_murata_uart_driver + RUNTIME DESTINATION /opt/mandeye/extras/) + +install(FILES services/mandeye_murata_uart_driver.service + DESTINATION /usr/lib/systemd/system) \ No newline at end of file diff --git a/extras/MurataUartImu/murataDriver.cpp b/extras/MurataUartImu/murataDriver.cpp new file mode 100644 index 0000000..dc22be9 --- /dev/null +++ b/extras/MurataUartImu/murataDriver.cpp @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ── Packet definition (must match firmware imu_pkt_t) ──────────────────────── +#pragma pack(push, 1) +struct ImuPacket +{ + uint8_t sync; + uint64_t timestamp_us; + int32_t rate[3]; + int32_t acc[3]; + int32_t temp; + uint8_t crc; +}; +#pragma pack(pop) + +static constexpr size_t PKT_SIZE = sizeof(ImuPacket); +static constexpr uint8_t SYNC_BYTE = 0xAA; +static constexpr double SENSITIVITY_RATE = 1600.0; +static constexpr double SENSITIVITY_ACC = 3200.0; + +static uint8_t crc8(const uint8_t* data, size_t len) +{ + uint8_t crc = 0; + for(size_t i = 0; i < len; i++) + { + crc ^= data[i]; + for(int j = 0; j < 8; j++) + crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : crc << 1; + } + return crc; +} + +inline double deg2rad(double deg) +{ + return deg * M_PI / 180.0; +} + +// ── Config / globals ───────────────────────────────────────────────────────── +namespace +{ +std::string getEnvString(const std::string& env, const std::string& def) +{ + const char* p = std::getenv(env.c_str()); + return p ? std::string{p} : def; +} +} // namespace + +namespace global +{ +std::string directoryName = "MURATA_IMU"; +int fileLengthMs = 20000; +std::string uartPort = "/dev/ttyUSB0"; +} // namespace global + +// ── Mode state ─────────────────────────────────────────────────────────────── +namespace MODES +{ +const static char* SCANNING = "SCANNING"; +const static char* STOPPING = "STOPPING"; +const static char* UNKNOWN = "UNKNOWN"; + +const auto SCANNING_ID = std::hash{}(SCANNING); +const auto STOPPING_ID = std::hash{}(STOPPING); +const auto UNKNOWN_ID = std::hash{}(UNKNOWN); +} // namespace MODES + +namespace state +{ +std::mutex stateMutex; +std::string modeName = MODES::UNKNOWN; +auto hashCode = MODES::UNKNOWN_ID; +std::string continuousScanTarget; +uint64_t timestamp; +} // namespace state + +// ── Config ─────────────────────────────────────────────────────────────────── +nlohmann::json getConfig(const std::string& configPath) +{ + if(!fs::exists(configPath)) + { + std::cerr << "Config file does not exist" << std::endl; + return {}; + } + std::ifstream f(configPath); + if(!f.is_open()) + { + std::cerr << "Failed to open config at " << configPath << std::endl; + return {}; + } + try + { + nlohmann::json j; + f >> j; + return j; + } + catch(const nlohmann::json::parse_error& e) + { + std::cerr << "Config parse error: " << e.what() << std::endl; + return {}; + } +} + +// ── ZMQ subscriber thread ──────────────────────────────────────────────────── +void clientThread() +{ + try + { + zmq::context_t ctx(1); + zmq::socket_t sock(ctx, zmq::socket_type::sub); + sock.connect("tcp://localhost:5556"); + sock.set(zmq::sockopt::subscribe, ""); + sock.set(zmq::sockopt::conflate, 1); + std::cout << "Connected to tcp://localhost:5556" << std::endl; + + while(true) + { + zmq::message_t msg; + if(!sock.recv(msg, zmq::recv_flags::none)) + continue; + + auto j = nlohmann::json::parse(std::string(static_cast(msg.data()), msg.size())); + if(!j.is_object()) + continue; + + std::lock_guard lk(state::stateMutex); + if(j.contains("time")) + state::timestamp = j["time"].get(); + if(j.contains("mode")) + { + state::modeName = j["mode"].get(); + state::hashCode = std::hash{}(state::modeName); + } + if(j.contains("continousScanDirectory")) + state::continuousScanTarget = j["continousScanDirectory"].get(); + } + } + catch(const zmq::error_t& e) + { + std::cerr << "ZMQ error: " << e.what() << std::endl; + std::abort(); + } +} + +// ── Row stored in memory ────────────────────────────────────────────────────── +struct ImuRow +{ + uint64_t ts_ns; + double gx, gy, gz; + double ax, ay, az; +}; + +// ── Flush buffer to file in a detached thread ───────────────────────────────── +void saveToFile(std::deque rows, std::string path) +{ + std::ofstream f(path); + if(!f) + { + std::cerr << "Failed to open " << path << std::endl; + return; + } + f << "timestamp gyroX gyroY gyroZ accX accY accZ timestampUnix\n"; + for(const auto& r : rows) + f << r.ts_ns << ' ' << r.gx << ' ' << r.gy << ' ' << r.gz << ' ' << r.ax << ' ' << r.ay << ' ' << r.az << ' ' << r.ts_ns << '\n'; + std::cout << "Saved " << rows.size() << " rows to " << path << std::endl; +} + +// ── IMU UART thread ────────────────────────────────────────────────────────── +int murataThread() +{ + LibSerial::SerialPort serial; + try + { + serial.Open(global::uartPort); + serial.SetBaudRate(LibSerial::BaudRate::BAUD_460800); + serial.SetCharacterSize(LibSerial::CharacterSize::CHAR_SIZE_8); + serial.SetParity(LibSerial::Parity::PARITY_NONE); + serial.SetStopBits(LibSerial::StopBits::STOP_BITS_1); + serial.SetFlowControl(LibSerial::FlowControl::FLOW_CONTROL_NONE); + } + catch(const LibSerial::OpenFailed&) + { + std::cerr << "Failed to open " << global::uartPort << std::endl; + return -1; + } + std::cout << "Opened " << global::uartPort << " @ 460800" << std::endl; + + std::deque buffer; + auto fileStartTime = std::chrono::steady_clock::now(); + auto statsTime = std::chrono::steady_clock::now(); + int pktCount = 0, errCount = 0; + bool collecting = false; + + uint8_t buf[PKT_SIZE]; + + for(;;) + { + // ── Sync on 0xAA ────────────────────────────────────────────────────── + try + { + uint8_t b; + serial.ReadByte(b, 0); + if(b != SYNC_BYTE) + { + errCount++; + continue; + } + buf[0] = b; + for(size_t i = 1; i < PKT_SIZE; i++) + serial.ReadByte(buf[i], 0); + } + catch(const LibSerial::ReadTimeout&) + { + continue; + } + + if(crc8(buf, PKT_SIZE - 1) != buf[PKT_SIZE - 1]) + { + errCount++; + continue; + } + + ImuPacket pkt; + memcpy(&pkt, buf, PKT_SIZE); + pktCount++; + + // ── Decode into row ─────────────────────────────────────────────────── + ImuRow row; + row.ts_ns = pkt.timestamp_us * 1000ULL; + row.gx = deg2rad(pkt.rate[0] / SENSITIVITY_RATE); + row.gy = deg2rad(pkt.rate[1] / SENSITIVITY_RATE); + row.gz = deg2rad(pkt.rate[2] / SENSITIVITY_RATE); + row.ax = pkt.acc[0] / SENSITIVITY_ACC; + row.ay = pkt.acc[1] / SENSITIVITY_ACC; + row.az = pkt.acc[2] / SENSITIVITY_ACC; + + // ── Read mode ───────────────────────────────────────────────────────── + size_t state_id; + std::string scanTarget; + { + std::lock_guard lk(state::stateMutex); + state_id = state::hashCode; + scanTarget = state::continuousScanTarget; + } + + // ── Accumulate to memory ────────────────────────────────────────────── + if(state_id == MODES::SCANNING_ID) + { + if(!collecting) + { + collecting = true; + fileStartTime = std::chrono::steady_clock::now(); + } + buffer.push_back(row); + } + + // ── Flush: time rotation or stop ────────────────────────────────────── + const bool timeUp = + collecting && + std::chrono::duration_cast(std::chrono::steady_clock::now() - fileStartTime).count() > global::fileLengthMs; + const bool stopping = collecting && (state_id == MODES::STOPPING_ID); + + if(timeUp || stopping) + { + const fs::path dir = fs::path(scanTarget) / global::directoryName; + fs::create_directories(dir); + std::string path = (dir / ("murata_imu_" + std::to_string(pkt.timestamp_us) + ".csv")).string(); + + // Move buffer ownership to a detached thread — no disk I/O on this thread + std::thread(saveToFile, std::move(buffer), std::move(path)).detach(); + buffer = {}; + + if(stopping) + collecting = false; + else + fileStartTime = std::chrono::steady_clock::now(); + } + + // ── Stats ───────────────────────────────────────────────────────────── + auto now = std::chrono::steady_clock::now(); + if(std::chrono::duration_cast(now - statsTime).count() >= 1) + { + double tp = std::chrono::duration(std::chrono::system_clock::now().time_since_epoch()).count(); + double imu_ts = double(pkt.timestamp_us) / 1000000.0; + std::cout << "pkts/s: " << pktCount << " errors: " << errCount << " diff: " << (tp - imu_ts) << " buffered: " << buffer.size() + << std::endl; + pktCount = errCount = 0; + statsTime = now; + } + } + return -1; +} + +// ── Main ───────────────────────────────────────────────────────────────────── +int main(int argc, char** argv) +{ + std::string configPath = getEnvString("EXTRA_MURATA_CONFIG_PATH", "/media/usb/config_murata_imu.json"); + global::directoryName = getEnvString("EXTRA_MURATA_DIRECTORY_NAME", "MURATA_IMU"); + global::uartPort = getEnvString("EXTRA_MURATA_UART_PORT", "/dev/ttyAMA0"); + + std::cout << "Config: " << configPath << std::endl; + auto configJson = getConfig(configPath); + if(configJson.is_object() && !configJson.empty()) + { + std::cout << configJson.dump(4) << std::endl; + if(configJson.contains("file_length_ms")) + global::fileLengthMs = configJson["file_length_ms"].get(); + if(configJson.contains("uart_port")) + global::uartPort = configJson["uart_port"].get(); + } + else + { + configJson["file_length_ms"] = global::fileLengthMs; + configJson["uart_port"] = global::uartPort; + std::ofstream f(configPath); + f << configJson.dump(4); + std::cout << "Created default config at " << configPath << std::endl; + } + std::cout << "UART: " << global::uartPort << std::endl; + + std::thread tzmq(clientThread); + std::thread timu(murataThread); + tzmq.join(); + timu.join(); +} \ No newline at end of file diff --git a/extras/MurataUartImu/services/mandeye_murata_uart_driver.service b/extras/MurataUartImu/services/mandeye_murata_uart_driver.service new file mode 100644 index 0000000..6d85447 --- /dev/null +++ b/extras/MurataUartImu/services/mandeye_murata_uart_driver.service @@ -0,0 +1,15 @@ +[Unit] +Description=mandeye_murata_uart_driver Service +After=multi-user.target + +[Service] +User=pi +StandardOutput=journal +StandardError=journal +ExecStartPre=/bin/sleep 20 +ExecStart=/opt/mandeye/extras/mandeye_murata_uart_driver +Restart=always + +[Install] +WantedBy=multi-user.target + diff --git a/extras/oled_status/CMakeLists.txt b/extras/oled_status/CMakeLists.txt index 864f9d2..9dda474 100644 --- a/extras/oled_status/CMakeLists.txt +++ b/extras/oled_status/CMakeLists.txt @@ -65,4 +65,7 @@ target_compile_definitions(u8g2_arm PRIVATE __ARM_LINUX__) # oled_status executable add_executable(oled_status u8g2_hw_i2c.cpp) target_link_libraries(oled_status u8g2_arm m mandeye_extra_utils) -target_compile_definitions(oled_status PRIVATE __ARM_LINUX__) \ No newline at end of file +target_compile_definitions(oled_status PRIVATE __ARM_LINUX__) + +install(FILES services/mandeye_oled_status.service DESTINATION /usr/lib/systemd/system) +install(TARGETS oled_status RUNTIME DESTINATION /opt/mandeye/extras/) \ No newline at end of file diff --git a/extras/rtkNmea/services/mandeye_extra_gnss.service b/extras/rtkNmea/services/mandeye_extra_gnss.service index edc9b2b..253aeb8 100644 --- a/extras/rtkNmea/services/mandeye_extra_gnss.service +++ b/extras/rtkNmea/services/mandeye_extra_gnss.service @@ -7,7 +7,7 @@ User=pi StandardOutput=journal StandardError=journal ExecStartPre=/bin/sleep 20 -ExecStart=/opt/mandeye/extras/mandeye_extra_gnss +ExecStart=/opt/mandeye/extras/mandeye_murata_spi_driver Restart=always [Install] diff --git a/packing/check_services.sh b/packing/check_services.sh index 53497f0..7f7a598 100755 --- a/packing/check_services.sh +++ b/packing/check_services.sh @@ -9,7 +9,7 @@ services=( mandeye_phc2sys-gm-eth0.service mandeye_extra_gnss.service mandeye_oled_status.service - + mandeye_murata_uart_driver.service ) echo "Checking Mandeye services..." diff --git a/packing/helpers.sh b/packing/helpers.sh index 1fe8809..c047412 100755 --- a/packing/helpers.sh +++ b/packing/helpers.sh @@ -45,4 +45,9 @@ alias mandeye_oled_start="sudo systemctl start mandeye_oled_status.service" alias mandeye_oled_stop="sudo systemctl stop mandeye_oled_status.service" alias mandeye_oled_log="journalctl -u mandeye_oled_status.service" +alias mandeye_murata_uart_imu_status="sudo systemctl status mandeye_murata_uart_driver.service" +alias mandeye_murata_uart_imu_start="sudo systemctl start mandeye_murata_uart_driver.service" +alias mandeye_murata_uart_imu_stop="sudo systemctl stop mandeye_murata_uart_driver.service" +alias mandeye_murata_uart_imu_log="journalctl -u mandeye_murata_uart_driver.service" + alias mandeye_services_status="bash /opt/mandeye/check_services.sh" diff --git a/packing/services/mandeye_fake_pps.service b/packing/services/mandeye_fake_pps.service deleted file mode 100644 index 8c02c6e..0000000 --- a/packing/services/mandeye_fake_pps.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Mandeye -After=multi-user.target - -[Service] -User=pi -ExecStartPre=/bin/sleep 5 -ExecStart=/opt/mandeye/fake_pps -Restart=always - -[Install] -WantedBy=multi-user.target