在 r 中解码声纳二进制数据

Decode sonar binary data in r

作为一名水文学家而不是程序员,我一直在解决简单的任务——从 LOWRENCE Fish Finder 中提取数据。它以二进制格式写入航迹、水深、温度等SL2. Which is a 144 bytes length with a 10 bytes header. There is a bunch of decoding implementations written in different languages (e.g. Java or Ruby)。

基于wiki, Ruby code and stack overflow I was trying to extract at least depth from this example file. Which in .csv can be found here.

我知道,depth 是从 60 开始的 4 字节浮点数。但是,this answer returns zero:

建议的代码
 # Open binary file
 toread <-  file("Chart 09_07_2018 [0].sl2", "rb")
 # all data
 alldata <- readBin(toread, raw(), n = 144, size = 1, endian = "little")
 # read WaterDepth
 readBin(alldata[59:65], double(), size = 4) 
> [1] 0
 close(toread)

如果我要使用 seek 函数,则相同 returns:

con <-  file("Chart 09_07_2018 [2].sl2", "rb")
seek(con, 60L)
readBin(con, double(), endian="little")
close(con)

文件中的数据存在 — 我能够通过 Sonar Viewer (see .csv example) 提取它。

制作 javascript/node.js 版本的小伙子修正了现场转录中的一些错误,值得一两枚奖章。

以下内容对于 grok 来说应该是相当简单的,但如果其中有任何需要 'splainin.您需要处理 longitude/latitude 编码(等)。

请注意,您可以跳过阅读函数源代码而直接执行:

devtools::install_git("https://gitlab.com/hrbrmstr/arabia")

library(arabia) # b/c I like puns way too much

read_sl2("your-sl2-file.sl2")

您可以在此处或此处查看源代码,不过:

read_sl2 <- function(path, verbose=TRUE) {

  f <- file(path.expand(path), "rb")
  dat <- readBin(f, "raw", n = file.size(path.expand(path)), endian="little")
  close(f)

  # read in the header
  header <- readBin(dat, what = "raw", n = 10)

  format <- readBin(header[1:2], "int", size=2, endian="little", signed=FALSE)

  if (!(format %in% 1:3)) stop("Invalid 'format' in header; Likely not an slg/sl2/sl3 file")

  ok_formats <- c("slg", "sl2", "sl3")
  if (verbose) message("Format: ", ok_formats[format])

  version <- readBin(header[3:4], "int", size=2, endian="little", signed=FALSE)
  blockSize <- readBin(header[5:6], "int", size=2, endian="little", signed=FALSE)

  if (blockSize == 1970) {
    if (verbose) message("Block size: downscan")
  } else if (blockSize == 3200) {
    if (verbose) message("Block size: sidescan")
  } else {
    stop("Block size is not 'downscan' or 'sidescan'; Likely not an slg/sl2/sl3 file")
  }

  alwaysZero <- readBin(header[7:8], "int", size=2, endian="little", signed=FALSE)

  # yep, we're going to build a list the hard/slow way
  sl2_lst <- vector("list")
  idx <- 1
  pos <- 8 # keeping track of our place in the stream

  while (pos < length(dat)) {

    # if verbose mode echo a "." every 100 records
    if (verbose && ((idx %% 100) == 0)) cat(".")

    blockSize <- readBin(dat[(pos+29):(pos+30)], "int", size=2, endian="little", signed=FALSE)
    prevBlockSize <- readBin(dat[(pos+31):(pos+32)], "int", size=2, endian="little", signed=FALSE)
    packetSize <- readBin(dat[(pos+35):(pos+36)], "int", size=2, endian="little", signed=FALSE)
    frameIndex <- readBin(dat[(pos+37):(pos+40)], "int", size=4, endian="little")

    dplyr::data_frame(
      channel = readBin(dat[(pos+33):(pos+34)], "int", size=2,endian="little", signed=FALSE),
      upperLimit = readBin(dat[(pos+41):(pos+44)], "double", size=4, endian="little"),
      lowerLimit = readBin(dat[(pos+45):(pos+48)], "double", size=4, endian="little"),
      frequency = readBin(dat[(pos+51)], "int", size=1, endian="little", signed=FALSE),
      waterDepth = readBin(dat[(pos+65):(pos+68)], "double", size=4, endian="little"),
      keelDepth = readBin(dat[(pos+69):(pos+72)], "double", size=4, endian="little"),
      speedGps = readBin(dat[(pos+101):(pos+104)], "double", size=4, endian="little"),
      temperature = readBin(dat[(pos+105):(pos+108)], "double", size=4, endian="little"),
      lng_enc = readBin(dat[(pos+109):(pos+112)], "integer", size=4, endian="little"),
      lat_enc = readBin(dat[(pos+113):(pos+116)], "integer", size=4, endian="little"),
      speedWater = readBin(dat[(pos+117):(pos+120)], "double", size=4, endian="little"),
      track = readBin(dat[(pos+121):(pos+124)], "double", size=4, endian="little"),
      altitude = readBin(dat[(pos+125):(pos+128)], "double", size=4, endian="little"),
      heading = readBin(dat[(pos+129):(pos+132)], "double", size=4, endian="little"),
      timeOffset = readBin(dat[(pos+141):(pos+144)], "integer", size=4, endian="little"),
      flags = list(
        dat[(pos+133):(pos+134)] %>%
          rawToBits() %>%
          as.logical() %>%
          set_names(
            c(
              "headingValid", "altitudeValid", sprintf("unk%d", 1:7),
              "gpsSpeedValid", "waterTempValid", "unk8", "positionValid",
              "unk9", "waterSpeedValid", "trackValid"
            )
          ) %>%
          .[c(1:2, 10:11, 13, 15:16)] %>%
          as.list() %>%
          purrr::flatten_df()
      )
    ) -> sl2_lst[[idx]]

    idx <- idx + 1

    pos <- pos + (packetSize+145-1)

  }

  if (verbose) cat("\n")

  dplyr::bind_rows(sl2_lst) %>%
    dplyr::mutate(
      channel = dplyr::case_when(
        channel == 0 ~ "Primary",
        channel == 1 ~ "Secondary",
        channel == 2 ~ "DSI (Downscan)",
        channel == 3 ~ "Left (Sidescan)",
        channel == 4 ~ "Right (Sidescan)",
        channel == 5 ~ "Composite",
        TRUE ~ "Other/invalid"
      )
    ) %>%
    dplyr::mutate(
      frequency = dplyr::case_when(
        frequency == 0 ~ "200 KHz",
        frequency == 1 ~ "50 KHz",
        frequency == 2 ~ "83 KHz",
        frequency == 4 ~ "800 KHz",
        frequency == 5 ~ "38 KHz",
        frequency == 6 ~ "28 KHz",
        frequency == 7 ~ "130-210 KHz",
        frequency == 8 ~ "90-150 KHz",
        frequency == 9 ~ "40-60 KHz",
        frequency == 10~ "25-45 KHz",
        TRUE ~ "Other/invalid"
      )
    ) %>%
    tidyr::unnest(flags)

}

试一试:

xdf <- read_sl2("~/Downloads/Chart 09_07_2018 [2].sl2")
## Format: sl2
## Block size: downscan
## .............

xdf
## # A tibble: 1,308 x 22
##    channel      upperLimit lowerLimit frequency waterDepth keelDepth speedGps temperature lng_enc lat_enc
##    <chr>             <dbl>      <dbl> <chr>          <dbl>     <dbl>    <dbl>       <dbl>   <int>   <int>
##  1 Secondary             0       13.3 200 KHz         2.62     0.328      0.5        15.8 4433307 7003054
##  2 DSI (Downsc…          0       13.4 200 KHz         2.62     0.328      0.5        15.8 4433307 7003054
##  3 Primary               0       13.3 200 KHz         2.62     0.328      0.5        15.9 4433307 7003054
##  4 Secondary             0       13.3 200 KHz         2.62     0.328      0.5        15.9 4433307 7003054
##  5 DSI (Downsc…          0       13.4 200 KHz         2.59     0.328      0          15.8 4433307 7003054
##  6 Secondary             0       13.3 200 KHz         2.59     0.328      0          15.8 4433307 7003054
##  7 Secondary             0       13.3 200 KHz         2.52     0.328      0          15.9 4433307 7003054
##  8 DSI (Downsc…          0       13.4 200 KHz         2.52     0.328      0          15.9 4433307 7003054
##  9 Primary               0       13.3 200 KHz         2.52     0.328      0          15.8 4433307 7003054
## 10 DSI (Downsc…          0       13.4 200 KHz         2.52     0.328      0          15.8 4433307 7003054
## # ... with 1,298 more rows, and 12 more variables: speedWater <dbl>, track <dbl>, altitude <dbl>,
## #   heading <dbl>, timeOffset <int>, headingValid <lgl>, altitudeValid <lgl>, gpsSpeedValid <lgl>,
## #   waterTempValid <lgl>, positionValid <lgl>, waterSpeedValid <lgl>, trackValid <lgl>

再看一眼:

glimpse(xdf)
## Observations: 1,308
## Variables: 22
## $ channel         <chr> "Secondary", "DSI (Downscan)", "Primary", "Secondary", "DSI (Downscan)", "Sec...
## $ upperLimit      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ lowerLimit      <dbl> 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.4, 13.3, 13.4, 13.3,...
## $ frequency       <chr> "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", ...
## $ waterDepth      <dbl> 2.620, 2.620, 2.620, 2.620, 2.586, 2.586, 2.516, 2.516, 2.516, 2.516, 2.516, ...
## $ keelDepth       <dbl> 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.32808...
## $ speedGps        <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ temperature     <dbl> 15.84112, 15.84112, 15.86293, 15.86293, 15.79128, 15.79128, 15.86293, 15.8629...
## $ lng_enc         <int> 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 44333...
## $ lat_enc         <int> 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 70030...
## $ speedWater      <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ track           <dbl> 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.97418...
## $ altitude        <dbl> 324.7375, 324.7375, 324.7375, 324.7375, 324.8687, 324.8687, 324.8687, 324.868...
## $ heading         <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ timeOffset      <int> 1317703, 1317706, 1318036, 1318905, 1318946, 1318982, 1319130, 1319140, 13192...
## $ headingValid    <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ altitudeValid   <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ gpsSpeedValid   <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ waterTempValid  <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ positionValid   <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ waterSpeedValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ trackValid      <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...

您可能想与您创建的 CSV 文件进行比较,因为我只是目测了一下。请注意,当它们在标志中标记为 "invalid" 时,您似乎使用了 "carried-forward" 值的导出器。我只是按原样保留它们。

如果您不是 "tidyverse" 那种人,"un-tidyverse"(留给提问者的练习)应该很容易。