File.fs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. namespace FSharp.Data.Tdms
  2. open System
  3. open System.Buffers
  4. open System.IO
  5. open System.Runtime.InteropServices
  6. open System.Text.RegularExpressions
  7. open System.Threading
  8. open System.Threading.Tasks
  9. open FSharp.Collections
  10. type File =
  11. { Path: string
  12. Properties: Property seq
  13. Groups: FSharp.Data.Tdms.Group seq }
  14. module File =
  15. [<Literal>]
  16. let LeadInLength = 28
  17. let ofObjects path objects =
  18. let groups =
  19. objects
  20. |> Seq.choose
  21. (fun ({ Name = groupName
  22. Properties = properties }: FSharp.Data.Tdms.Object) ->
  23. if Regex.IsMatch(groupName, @"^\/'[^\/']+'$") then
  24. Some
  25. { Name = groupName.Substring(2, String.length groupName - 3)
  26. Properties = properties
  27. Channels =
  28. objects
  29. |> Seq.filter
  30. (fun { Name = channelName } ->
  31. channelName.StartsWith groupName
  32. && String.length channelName > String.length groupName)
  33. |> Seq.map (Object.toChannel path) }
  34. else
  35. None)
  36. { Path = path
  37. Properties =
  38. objects
  39. |> Seq.tryFind (fun ({ Name = name }: FSharp.Data.Tdms.Object) -> name = "/")
  40. |> Option.map (fun { Properties = properties } -> properties)
  41. |> Option.toList
  42. |> Seq.concat
  43. Groups = groups }
  44. /// <summary>
  45. /// Opens a <see cref="File" />, reads the index from it, and closes it.
  46. /// </summary>
  47. /// <param name="path">the path to the <see cref="File" /> to read.</param>
  48. /// <param name="writeIndex">Whether to write the TDMS index file if it does not exist.</param>
  49. let read path writeIndex =
  50. let indexPath =
  51. Path.ChangeExtension(path, ".tdms_index")
  52. let indexExists = File.Exists indexPath
  53. use stream =
  54. new FileStream(
  55. (if indexExists then indexPath else path),
  56. FileMode.Open,
  57. FileAccess.Read,
  58. FileShare.Read,
  59. 65_536,
  60. if indexExists then
  61. FileOptions.SequentialScan
  62. else
  63. FileOptions.None
  64. )
  65. use indexStream =
  66. if not indexExists && writeIndex then
  67. new FileStream(indexPath, FileMode.Create, FileAccess.Write, FileShare.None, 8_192, false)
  68. else
  69. null
  70. let mutable buffer = ArrayPool<byte>.Shared.Rent LeadInLength
  71. let objects = ResizeArray()
  72. let mutable offset = 0uL
  73. while stream.Position < stream.Length do
  74. stream.Read(buffer, 0, LeadInLength)
  75. |> ignore
  76. if not indexExists && writeIndex then
  77. indexStream.Write(Segment.tdsh, 0, 4)
  78. indexStream.Write(buffer, 4, 24)
  79. let mutable leadInSpan = ReadOnlySpan buffer
  80. let { TableOfContents = tableOfContents
  81. NextSegmentOffset = nextSegmentOffset
  82. RawDataOffset = rawDataOffset } =
  83. Segment.readLeadIn &leadInSpan
  84. let metaDataStart = offset + 28uL
  85. if tableOfContents.HasFlag(TableOfContents.ContainsMetaData) then
  86. let remainingLength = int rawDataOffset
  87. if remainingLength > buffer.Length then
  88. ArrayPool<byte>.Shared.Return (buffer, false)
  89. buffer <- ArrayPool<byte>.Shared.Rent remainingLength
  90. stream.Read(buffer, 0, remainingLength) |> ignore
  91. if not indexExists && writeIndex then
  92. indexStream.Write(buffer, 0, remainingLength)
  93. let mutable metaDataSpan = ReadOnlySpan buffer
  94. Segment.readMetaData
  95. objects
  96. (metaDataStart + rawDataOffset)
  97. (metaDataStart + min nextSegmentOffset (uint64 stream.Length - metaDataStart))
  98. &metaDataSpan
  99. (tableOfContents.HasFlag(TableOfContents.ContainsBigEndianData))
  100. (tableOfContents.HasFlag(TableOfContents.ContainsInterleavedData))
  101. offset <- metaDataStart + nextSegmentOffset
  102. if not indexExists then
  103. stream.Seek(int64 offset, SeekOrigin.Begin)
  104. |> ignore
  105. ArrayPool<byte>.Shared.Return (buffer, false)
  106. ofObjects path objects
  107. let readAsyncCt ct path writeIndex =
  108. task {
  109. let indexPath =
  110. Path.ChangeExtension(path, ".tdms_index")
  111. let indexExists = File.Exists(indexPath)
  112. use stream =
  113. new FileStream(
  114. (if indexExists then indexPath else path),
  115. FileMode.Open,
  116. FileAccess.Read,
  117. FileShare.Read,
  118. 65_536,
  119. if indexExists then
  120. FileOptions.SequentialScan
  121. ||| FileOptions.Asynchronous
  122. else
  123. FileOptions.Asynchronous
  124. )
  125. use indexStream =
  126. if not indexExists && writeIndex then
  127. new FileStream(indexPath, FileMode.Create, FileAccess.Write, FileShare.None, 1_048_576, true)
  128. else
  129. null
  130. let mutable buffer = ArrayPool<byte>.Shared.Rent LeadInLength
  131. let objects = ResizeArray()
  132. let mutable offset = 0uL
  133. while stream.Position < stream.Length do
  134. let! _ = stream.ReadAsync(buffer, 0, LeadInLength, ct)
  135. if not indexExists && writeIndex then
  136. do! indexStream.WriteAsync(Segment.tdsh, 0, 4, ct)
  137. do! indexStream.WriteAsync(buffer, 4, 24, ct)
  138. let mutable leadInSpan = ReadOnlySpan buffer
  139. let { TableOfContents = tableOfContents
  140. NextSegmentOffset = nextSegmentOffset
  141. RawDataOffset = rawDataOffset } =
  142. Segment.readLeadIn &leadInSpan
  143. let metaDataStart = offset + 28uL
  144. if tableOfContents.HasFlag(TableOfContents.ContainsMetaData) then
  145. let remainingLength = int rawDataOffset
  146. if remainingLength > buffer.Length then
  147. ArrayPool<byte>.Shared.Return (buffer, false)
  148. buffer <- ArrayPool<byte>.Shared.Rent remainingLength
  149. let! _ = stream.ReadAsync(buffer, 0, remainingLength, ct)
  150. if not indexExists && writeIndex then
  151. do! indexStream.WriteAsync(buffer, 0, remainingLength, ct)
  152. let mutable metaDataSpan = ReadOnlySpan buffer
  153. Segment.readMetaData
  154. objects
  155. (metaDataStart + rawDataOffset)
  156. (metaDataStart + nextSegmentOffset)
  157. &metaDataSpan
  158. (tableOfContents.HasFlag(TableOfContents.ContainsBigEndianData))
  159. (tableOfContents.HasFlag(TableOfContents.ContainsInterleavedData))
  160. offset <- metaDataStart + nextSegmentOffset
  161. if not indexExists then
  162. stream.Seek(int64 offset, SeekOrigin.Begin)
  163. |> ignore
  164. ArrayPool<byte>.Shared.Return (buffer, false)
  165. return ofObjects path objects
  166. }
  167. /// <summary>
  168. /// Asynchronously opens a <see cref="File" />, reads the index from it, and closes it.
  169. /// </summary>
  170. /// <param name="path">the path to the <see cref="File" /> to read.</param>
  171. /// <param name="writeIndex">Whether to write the index file if it does not exist.</param>
  172. let readAsync = readAsyncCt CancellationToken.None
  173. let tryGetPropertyValue<'t> propertyName ({ Properties = properties }: File) =
  174. properties
  175. |> Seq.tryFind (fun { Name = propertyName' } -> propertyName' = propertyName)
  176. |> Option.bind Property.tryGet<'t>
  177. let getPropertyValue<'t> propertyName =
  178. tryGetPropertyValue<'t> propertyName >> Option.get
  179. /// <summary>Returns all groups within the <see cref="File" />.</summary>
  180. let getGroups { Groups = groups } = groups
  181. /// <summary>Returns the <see cref="Group" /> with the given name within the <see cref="File" />. Returns None if it does not exist.</summary>
  182. /// <param name="groupName">the name of the <see cref="Group" /> to find.</param>
  183. let tryFindGroup groupName =
  184. getGroups >> Seq.tryFind (fun { Name = groupName' } -> groupName' = groupName)
  185. /// <summary>Returns the <see cref="Group" /> with the given name within the <see cref="File" />.</summary>
  186. /// <param name="groupName">the name of the <see cref="Group" /> to find.</param>
  187. let findGroup groupName = tryFindGroup groupName >> Option.get
  188. /// <summary>Returns the <see cref="Channel" /> with the given name within the <see cref="Group" /> with the given name within the <see cref="File" />. Returns None if it does not exist.</summary>
  189. /// <param name="groupName">the name of the <see cref="Group" /> to find the <see cref="Channel" /> in.</param>
  190. /// <param name="channelName">the name of the <see cref="Channel" /> to find.</param>
  191. let tryFindChannel groupName channelName =
  192. tryFindGroup groupName
  193. >> Option.bind (Group.tryFindChannel channelName)
  194. /// <summary>Returns the <see cref="Channel" /> with the given name within the <see cref="Group" /> with the given name.</summary>
  195. /// <param name="groupName">the name of the <see cref="Group" /> to find the <see cref="Channel" /> in.</param>
  196. /// <param name="channelName">the name of the <see cref="Channel" /> to find.</param>
  197. let findChannel groupName channelName =
  198. tryFindChannel groupName channelName >> Option.get
  199. /// <summary>Returns the raw data for a <see cref="Channel" />. Returns None if the <see cref="Channel" /> does not exist, if it does not have any raw data, or if its raw data is not of the given type.</summary>
  200. /// <param name="groupName">the name of the <see cref="Group" /> the <see cref="Channel" /> is in.</param>
  201. /// <param name="channelName">the name of the <see cref="Channel" /> to get raw data for.</param>
  202. /// <param name="file">the TDMS file to read from.</param>
  203. let tryGetRawData<'t> groupName channelName file =
  204. tryFindChannel groupName channelName file
  205. |> Option.bind Channel.tryGetRawData<'t>
  206. #if !IS_DESIGNTIME
  207. /// <summary>Asynchronously returns the raw data for a <see cref="Channel" />. Returns None if the <see cref="Channel" /> does not exist, if it does not have any raw data, or if its raw data is not of the given type.</summary>
  208. /// <param name="groupName">the name of the <see cref="Group" /> the <see cref="Channel" /> is in.</param>
  209. /// <param name="channelName">the name of the <see cref="Channel" /> to get raw data for.</param>
  210. /// <param name="file">the TDMS file to read from.</param>
  211. let tryGetRawDataAsyncCt<'t> ct groupName channelName file =
  212. tryFindChannel groupName channelName file
  213. |> Option.map (Channel.tryGetRawDataAsyncCt<'t> ct)
  214. |> Option.defaultValue (Task.FromResult None)
  215. let tryGetRawDataAsync<'t> = tryGetRawDataAsyncCt<'t> CancellationToken.None
  216. #endif
  217. type File with
  218. /// <summary>
  219. /// Opens a TDMS file, reads the index from it, and closes it.
  220. /// </summary>
  221. /// <param name="path"> The path to the TDMS file to read.</param>
  222. /// <param name="writeIndex"> Whether to write the TDMS index file.</param>
  223. static member Read(path, writeIndex) = File.read path writeIndex
  224. /// <summary>
  225. /// Asynchronously opens a TDMS file, reads the index from it, and closes it.
  226. /// </summary>
  227. /// <param name="path"> The path to the TDMS file to read.</param>
  228. /// <param name="writeIndex"> Whether to write the TDMS index file.</param>
  229. static member ReadAsync(path, writeIndex, [<Optional; DefaultParameterValue(CancellationToken())>] ct) = File.readAsyncCt ct path writeIndex
  230. /// <summary>
  231. /// Tries to get the raw data for the given channel, belonging to the given group in the given TDMS file.
  232. /// </summary>
  233. member file.TryGetRawData<'T>(groupName, channelName, [<Out>] rawData: byref<'T []>) =
  234. match File.tryGetRawData<'T> groupName channelName file with
  235. | None -> false
  236. | Some rd ->
  237. rawData <- rd
  238. true
  239. #if !IS_DESIGNTIME
  240. /// <summary>
  241. /// Asynchronously gets the raw data for the given channel, belonging to the given group in the given TDMS file.
  242. /// </summary>
  243. member file.GetRawDataAsync<'t>(groupName, channelName, [<Optional; DefaultParameterValue(CancellationToken())>] ct) =
  244. backgroundTask {
  245. match! File.tryGetRawDataAsyncCt<'t> ct groupName channelName file with
  246. | None -> return null
  247. | Some rd -> return rd
  248. }
  249. #endif
  250. /// <summary>
  251. /// Tries to get a property value for the given TDMS file.
  252. /// </summary>
  253. member file.TryGetPropertyValue<'T>(propertyName, [<Out>] propertyValue: byref<'T>) =
  254. match File.tryGetPropertyValue<'T> propertyName file with
  255. | None -> false
  256. | Some pv ->
  257. propertyValue <- pv
  258. true
  259. /// <summary>
  260. /// Tries to get a property value for the given group in the given TDMS file.
  261. /// </summary>
  262. member file.TryGetPropertyValue<'T>(propertyName, groupName, [<Out>] propertyValue: byref<'T>) =
  263. match File.tryFindGroup groupName file
  264. |> Option.bind (Group.tryGetPropertyValue<'T> propertyName) with
  265. | None -> false
  266. | Some pv ->
  267. propertyValue <- pv
  268. true
  269. /// <summary>
  270. /// Tries to get a property value for the given channel, belonging to the given group in the given TDMS file.
  271. /// </summary>
  272. member file.TryGetPropertyValue<'T>(propertyName, groupName, channelName, [<Out>] propertyValue: byref<'T>) =
  273. match File.tryGetPropertyValue<'T> ("/" + groupName + "/" + channelName) file with
  274. | None -> false
  275. | Some pv ->
  276. propertyValue <- pv
  277. true