[{"data":1,"prerenderedAt":1647},["ShallowReactive",2],{"blog-\u002Fblog\u002F2025\u002F11\u002Fcsv-mqtt-database-dashboard-flowfuse":3},{"id":4,"title":5,"body":6,"description":12,"extension":1637,"meta":1638,"navigation":269,"path":1643,"seo":1644,"stem":1645,"__hash__":1646},"blog\u002Fblog\u002F2025\u002F11\u002Fcsv-mqtt-database-dashboard-flowfuse.md","How to Ingest CSV Logs into MQTT, Databases, and Dashboards",{"type":7,"value":8,"toc":1619},"minimark",[9,13,16,19,22,27,30,53,60,64,67,72,77,80,84,87,126,138,159,166,170,173,200,209,212,216,219,230,370,374,377,381,388,393,409,500,504,545,553,556,562,571,579,583,587,590,594,597,644,648,670,674,677,687,884,888,891,895,904,907,911,914,990,994,997,1010,1452,1457,1460,1464,1471,1475,1505,1509,1530,1537,1541,1544,1569,1578,1585,1588,1597,1601,1604,1607,1615],[10,11,12],"p",{},"If you work in manufacturing, you likely have gigabytes of data in CSV files—temperature logs, production counts, machine status records. The data exists and is organized, but it's not accessible to the systems that need it.",[10,14,15],{},"PLCs log directly to CSV through proprietary software. Legacy SCADA systems write flat files. Custom applications generate daily reports. These systems are reliable, but they weren't built to integrate with MQTT brokers or databases.",[10,17,18],{},"If you're using FlowFuse to log data, you can send to MQTT and databases as data flows through FlowFuse. But if your CSV files come from PLCs, SCADA systems, or other external tools, you need to read and ingest them after they're written.",[10,20,21],{},"This guide shows you how to read CSV files—whether real-time or historical—and route them to MQTT brokers, databases, and dashboards.",[23,24,26],"h3",{"id":25},"prerequisites","Prerequisites",[10,28,29],{},"Before starting, ensure you have:",[31,32,33,44,47,50],"ul",{},[34,35,36,37],"li",{},"A running FlowFuse instance. If you don't have one, ",[38,39,43],"a",{"href":40,"rel":41},"https:\u002F\u002Fapp.flowfuse.com\u002F",[42],"nofollow","sign up for free",[34,45,46],{},"CSV log files with industrial data",[34,48,49],{},"Access to an MQTT broker",[34,51,52],{},"Access to a database",[10,54,55,59],{},[56,57,58],"strong",{},"Note:"," If you have an Enterprise account, you won't need to set up external MQTT brokers or databases, FlowFuse provides these services built into the platform.",[23,61,63],{"id":62},"sample-data","Sample Data",[10,65,66],{},"If you don't have a CSV file and want to follow along, you can import the following flow and deploy it. This flow will simulate a temperature sensor and log readings to a daily CSV file.",[68,69],"render-flow",{":height":70,"flow":71},"300","W3siaWQiOiI2Njc3NjM4NWRiNTc5NGJjIiwidHlwZSI6Imdyb3VwIiwieiI6IjVhMDA4MGQxMzRmODBmN2QiLCJuYW1lIjoiIiwic3R5bGUiOnsiZmlsbCI6Im5vbmUiLCJsYWJlbCI6dHJ1ZSwiZmlsbC1vcGFjaXR5IjoiMC41NyJ9LCJub2RlcyI6WyJhYzBkMzVhNjQ2NmNmY2I0IiwiNGFmZjViNTdjYmI2M2I4ZiIsImE1YzU3NDY5MzQ2NzAzMDYiLCIyM2ViYzBkYTQzMTVhYzQ2IiwiMjUxOGRjOTA5ZDQ0NzY1NSJdLCJ4IjoyMTQsInkiOjI3OSwidyI6ODkyLCJoIjo4Mn0seyJpZCI6ImFjMGQzNWE2NDY2Y2ZjYjQiLCJ0eXBlIjoiY3N2IiwieiI6IjVhMDA4MGQxMzRmODBmN2QiLCJnIjoiNjY3NzYzODVkYjU3OTRiYyIsIm5hbWUiOiIiLCJzcGVjIjoicmZjIiwic2VwIjoiLCIsImhkcmluIjp0cnVlLCJoZHJvdXQiOiJvbmNlIiwibXVsdGkiOiJvbmUiLCJyZXQiOiJcXHIiLCJ0ZW1wIjoidGltZXN0YW1wLHRlbXBlcmF0dXJlIiwic2tpcCI6IjAiLCJzdHJpbmdzIjp0cnVlLCJpbmNsdWRlX2VtcHR5X3N0cmluZ3MiOiIiLCJpbmNsdWRlX251bGxfdmFsdWVzIjoiIiwieCI6NjcwLCJ5IjozMjAsIndpcmVzIjpbWyJhNWM1NzQ2OTM0NjcwMzA2Il1dfSx7ImlkIjoiNGFmZjViNTdjYmI2M2I4ZiIsInR5cGUiOiJmdW5jdGlvbiIsInoiOiI1YTAwODBkMTM0ZjgwZjdkIiwiZyI6IjY2Nzc2Mzg1ZGI1Nzk0YmMiLCJuYW1lIjoiRGFpbHkgUExDIExvZ2dlciIsImZ1bmMiOiIvLyBAdHMtaWdub3JlIE5vZGUg4omlIDE4LjE1IHByb3ZpZGVzIGZzLnN0YXRmc1N5bmM7IGVkaXRvciB0eXBlcyBtYXkgbGFnXG5cbmNvbnN0IG5vdyA9IG5ldyBEYXRlKCk7XG5jb25zdCBkYXRlU3RyID0gbm93LnRvSVNPU3RyaW5nKCkuc3BsaXQoJ1QnKVswXTtcbmNvbnN0IHRpbWVzdGFtcCA9IG5vdy50b0lTT1N0cmluZygpO1xuXG5jb25zdCBmaWxlbmFtZSA9IGAuL3BsY19kYXRhXyR7ZGF0ZVN0cn0uY3N2YDtcblxubXNnLnBheWxvYWQgPSB7XG4gICAgdGltZXN0YW1wOiB0aW1lc3RhbXAsXG4gICAgdGVtcGVyYXR1cmU6IG1zZy5wYXlsb2FkLFxufTtcblxubXNnLmZpbGVuYW1lID0gZmlsZW5hbWU7XG5cbi8vIFRyYWNrIGxhc3QgZGF0ZSBpbiBmbG93IGNvbnRleHRcbmNvbnN0IGxhc3REYXRlID0gZmxvdy5nZXQoJ2xhc3REYXRlJykgfHwgJyc7XG5pZiAobGFzdERhdGUgIT09IGRhdGVTdHIpIHtcbiAgICBtc2cucmVzZXQgPSB0cnVlOyAvLyBXaWxsIHRyaWdnZXIgQ1NWIG5vZGUgdG8gd3JpdGUgaGVhZGVyc1xuICAgIGZsb3cuc2V0KCdsYXN0RGF0ZScsIGRhdGVTdHIpO1xufSBcblxucmV0dXJuIG1zZztcbiIsIm91dHB1dHMiOjEsInRpbWVvdXQiOjAsIm5vZXJyIjowLCJpbml0aWFsaXplIjoiIiwiZmluYWxpemUiOiIiLCJsaWJzIjpbXSwieCI6NDkwLCJ5IjozMjAsIndpcmVzIjpbWyJhYzBkMzVhNjQ2NmNmY2I0Il1dfSx7ImlkIjoiYTVjNTc0NjkzNDY3MDMwNiIsInR5cGUiOiJmaWxlIiwieiI6IjVhMDA4MGQxMzRmODBmN2QiLCJnIjoiNjY3NzYzODVkYjU3OTRiYyIsIm5hbWUiOiJMb2cgRGF0YSB0byBDU1YgZmlsZSIsImZpbGVuYW1lIjoiZmlsZW5hbWUiLCJmaWxlbmFtZVR5cGUiOiJtc2ciLCJhcHBlbmROZXdsaW5lIjp0cnVlLCJjcmVhdGVEaXIiOnRydWUsIm92ZXJ3cml0ZUZpbGUiOiJmYWxzZSIsImVuY29kaW5nIjoibm9uZSIsIngiOjg0MCwieSI6MzIwLCJ3aXJlcyI6W1siMjUxOGRjOTA5ZDQ0NzY1NSJdXX0seyJpZCI6IjIzZWJjMGRhNDMxNWFjNDYiLCJ0eXBlIjoiaW5qZWN0IiwieiI6IjVhMDA4MGQxMzRmODBmN2QiLCJnIjoiNjY3NzYzODVkYjU3OTRiYyIsIm5hbWUiOiIiLCJwcm9wcyI6W3sicCI6InBheWxvYWQifV0sInJlcGVhdCI6IjUiLCJjcm9udGFiIjoiIiwib25jZSI6ZmFsc2UsIm9uY2VEZWxheSI6MC4xLCJ0b3BpYyI6IiIsInBheWxvYWQiOiIkcm91bmQoNzAgKyAoJHJhbmRvbSgpICogMiksIDIpIiwicGF5bG9hZFR5cGUiOiJqc29uYXRhIiwieCI6MzEwLCJ5IjozMjAsIndpcmVzIjpbWyI0YWZmNWI1N2NiYjYzYjhmIl1dfSx7ImlkIjoiMjUxOGRjOTA5ZDQ0NzY1NSIsInR5cGUiOiJkZWJ1ZyIsInoiOiI1YTAwODBkMTM0ZjgwZjdkIiwiZyI6IjY2Nzc2Mzg1ZGI1Nzk0YmMiLCJuYW1lIjoiUmVzdWx0IiwiYWN0aXZlIjp0cnVlLCJ0b3NpZGViYXIiOnRydWUsImNvbnNvbGUiOmZhbHNlLCJ0b3N0YXR1cyI6ZmFsc2UsImNvbXBsZXRlIjoicGF5bG9hZCIsInRhcmdldFR5cGUiOiJtc2ciLCJzdGF0dXNWYWwiOiIiLCJzdGF0dXNUeXBlIjoiYXV0byIsIngiOjEwMTAsInkiOjMyMCwid2lyZXMiOltdfV0=",[73,74,76],"h2",{"id":75},"real-time-data-pipeline","Real-Time Data Pipeline",[10,78,79],{},"This pipeline monitors CSV files for changes and immediately publishes new data to MQTT topics. Use this approach when you need live data streams for dashboards, alerting systems, or real-time coordination between multiple systems.",[23,81,83],{"id":82},"step-1-monitoring-csv-files","Step 1: Monitoring CSV Files",[10,85,86],{},"The Watch node continuously monitors a directory and triggers whenever a file is created or modified.",[88,89,90,102,117],"ol",{},[34,91,92,93,96,97,101],{},"Drag the ",[56,94,95],{},"Watch node"," onto the canvas and double-click to configure it. Enter the directory path you want to monitor in the Files field, such as ",[98,99,100],"code",{},"\u002Fhome\u002Fuser\u002Fdata\u002F",".",[34,103,104,105,108,109,112,113,116],{},"Drag a ",[56,106,107],{},"Change node"," onto the canvas to set the correct filename. Configure it to set ",[98,110,111],{},"msg.filename"," using a JSONata expression: ",[98,114,115],{},"\"plc_data_\" & $moment().format(\"YYYY-MM-DD\") & \".csv\""," which dynamically constructs the filename based on today's date. If your files use a different naming convention, adjust the expression accordingly.",[34,118,104,119,122,123,125],{},[56,120,121],{},"Read File node"," onto the canvas to read the file contents. Set the Filename field to ",[98,124,111],{}," as a message property. Configure the Output as \"a single utf8 string\" and leave Encoding at Default.",[10,127,128,134],{},[129,130],"img",{"alt":131,"dataZoomable":132,"src":133},"Configuration screen of the Read File node showing filename set to msg.filename, output format as utf8 string, and default encoding","","\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fread-file-node.png",[135,136,137],"em",{},"Configuration of the File In node to read CSV file contents",[88,139,141,156],{"start":140},4,[34,142,143,144,146,147,149,150,146,152,155],{},"Connect the output of the ",[56,145,95],{}," to the input of the ",[56,148,107],{},", and the output of the ",[56,151,107],{},[56,153,154],{},"Read File"," node.",[34,157,158],{},"Deploy the flow.",[10,160,161,162,165],{},"At this point, whenever a CSV file in your monitored directory is created or modified, the flow reads its contents. The raw CSV data is now in ",[98,163,164],{},"msg.payload"," as a text string, ready to be parsed.",[23,167,169],{"id":168},"step-2-parsing-csv-data","Step 2: Parsing CSV Data",[10,171,172],{},"Now we'll convert the raw CSV text into structured data.",[88,174,175],{},[34,176,104,177,180,181],{},[56,178,179],{},"CSV node"," onto the canvas and connect it to the File In node output. Double-click to configure it to set:\n",[31,182,183,186,189,192],{},[34,184,185],{},"Separator: Comma",[34,187,188],{},"Parser: RFC4180",[34,190,191],{},"Check \"First row contains column names\" and \"Parse numerical values\"",[34,193,194,195,199],{},"Output: \"a single message ",[196,197,198],"span",{},"array","\"",[10,201,202,206],{},[129,203],{"alt":204,"dataZoomable":132,"src":205},"CSV node configuration showing comma separator, RFC4180 parser, first row as column names, parse numerical values enabled, and output as single message array","\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fcsv-node-config.png",[135,207,208],{},"CSV parser configuration for batch processing with RFC4180 standard",[10,210,211],{},"The CSV node now converts the raw text into an array of objects, where each object represents a row with named properties from the column headers.",[23,213,215],{"id":214},"step-3-extracting-the-latest-record","Step 3: Extracting the Latest Record",[10,217,218],{},"Since we’re reading the entire file each time it changes, we only need to publish the most recent data point to MQTT.",[88,220,221,227],{},[34,222,104,223,226],{},[56,224,225],{},"Function node"," onto the canvas and connect it to the CSV node output.",[34,228,229],{},"Add this code to extract only the latest row:",[231,232,236],"pre",{"className":233,"code":234,"language":235,"meta":132,"style":132},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F msg.payload is an array of all CSV rows\nconst rows = msg.payload;\n\n\u002F\u002F Extract only the latest row\nif (rows && rows.length > 0) {\n    msg.payload = rows[rows.length - 1];\n    return msg;\n} else {\n    \u002F\u002F No data in file yet\n    return null;\n}\n","javascript",[98,237,238,246,264,271,276,303,326,335,347,353,364],{"__ignoreMap":132},[196,239,242],{"class":240,"line":241},"line",1,[196,243,245],{"class":244},"sJ8bj","\u002F\u002F msg.payload is an array of all CSV rows\n",[196,247,249,253,257,260],{"class":240,"line":248},2,[196,250,252],{"class":251},"szBVR","const",[196,254,256],{"class":255},"sj4cs"," rows",[196,258,259],{"class":251}," =",[196,261,263],{"class":262},"sVt8B"," msg.payload;\n",[196,265,267],{"class":240,"line":266},3,[196,268,270],{"emptyLinePlaceholder":269},true,"\n",[196,272,273],{"class":240,"line":140},[196,274,275],{"class":244},"\u002F\u002F Extract only the latest row\n",[196,277,279,282,285,288,291,294,297,300],{"class":240,"line":278},5,[196,280,281],{"class":251},"if",[196,283,284],{"class":262}," (rows ",[196,286,287],{"class":251},"&&",[196,289,290],{"class":262}," rows.",[196,292,293],{"class":255},"length",[196,295,296],{"class":251}," >",[196,298,299],{"class":255}," 0",[196,301,302],{"class":262},") {\n",[196,304,306,309,312,315,317,320,323],{"class":240,"line":305},6,[196,307,308],{"class":262},"    msg.payload ",[196,310,311],{"class":251},"=",[196,313,314],{"class":262}," rows[rows.",[196,316,293],{"class":255},[196,318,319],{"class":251}," -",[196,321,322],{"class":255}," 1",[196,324,325],{"class":262},"];\n",[196,327,329,332],{"class":240,"line":328},7,[196,330,331],{"class":251},"    return",[196,333,334],{"class":262}," msg;\n",[196,336,338,341,344],{"class":240,"line":337},8,[196,339,340],{"class":262},"} ",[196,342,343],{"class":251},"else",[196,345,346],{"class":262}," {\n",[196,348,350],{"class":240,"line":349},9,[196,351,352],{"class":244},"    \u002F\u002F No data in file yet\n",[196,354,356,358,361],{"class":240,"line":355},10,[196,357,331],{"class":251},[196,359,360],{"class":255}," null",[196,362,363],{"class":262},";\n",[196,365,367],{"class":240,"line":366},11,[196,368,369],{"class":262},"}\n",[88,371,372],{"start":266},[34,373,158],{},[10,375,376],{},"Your parsed data now flows through as individual records, ready to be published to MQTT.",[23,378,380],{"id":379},"step-4-publishing-to-mqtt","Step 4: Publishing to MQTT",[10,382,383,384,101],{},"Now we'll publish the parsed CSV data to an MQTT broker for real-time distribution. If you have FlowFuse Team or Enterprise tier, ",[38,385,387],{"href":386},"\u002Fblog\u002F2025\u002F10\u002Fplc-to-mqtt-using-flowfuse\u002F#step-3%3A-set-up-mqtt-with-flowfuse","enable the MQTT broker within your FlowFuse team",[389,390,392],"h4",{"id":391},"adding-context-to-your-data","Adding Context to Your Data",[88,394,395,403],{},[34,396,104,397,399,400,402],{},[56,398,107],{}," onto the canvas and connect it to the output of the ",[56,401,225],{},". This step adds useful context for better data traceability.",[34,404,405,406,408],{},"Configure the node to set ",[98,407,164],{}," to a structured object—customize it based on your data and application needs:",[231,410,412],{"className":233,"code":411,"language":235,"meta":132,"style":132},"{\n    \"timestamp\": payload.timestamp,\n    \"site\": \"tokyo_plant\",\n    \"line\": \"assembly_line_a\",\n    \"device\": \"press_01\",\n    \"measurement\": \"temperature\",\n    \"value\": payload.temperature,\n    \"unit\": \"celsius\"\n}\n",[98,413,414,419,428,442,454,466,478,486,496],{"__ignoreMap":132},[196,415,416],{"class":240,"line":241},[196,417,418],{"class":262},"{\n",[196,420,421,425],{"class":240,"line":248},[196,422,424],{"class":423},"sZZnC","    \"timestamp\"",[196,426,427],{"class":262},": payload.timestamp,\n",[196,429,430,433,436,439],{"class":240,"line":266},[196,431,432],{"class":423},"    \"site\"",[196,434,435],{"class":262},": ",[196,437,438],{"class":423},"\"tokyo_plant\"",[196,440,441],{"class":262},",\n",[196,443,444,447,449,452],{"class":240,"line":140},[196,445,446],{"class":423},"    \"line\"",[196,448,435],{"class":262},[196,450,451],{"class":423},"\"assembly_line_a\"",[196,453,441],{"class":262},[196,455,456,459,461,464],{"class":240,"line":278},[196,457,458],{"class":423},"    \"device\"",[196,460,435],{"class":262},[196,462,463],{"class":423},"\"press_01\"",[196,465,441],{"class":262},[196,467,468,471,473,476],{"class":240,"line":305},[196,469,470],{"class":423},"    \"measurement\"",[196,472,435],{"class":262},[196,474,475],{"class":423},"\"temperature\"",[196,477,441],{"class":262},[196,479,480,483],{"class":240,"line":328},[196,481,482],{"class":423},"    \"value\"",[196,484,485],{"class":262},": payload.temperature,\n",[196,487,488,491,493],{"class":240,"line":337},[196,489,490],{"class":423},"    \"unit\"",[196,492,435],{"class":262},[196,494,495],{"class":423},"\"celsius\"\n",[196,497,498],{"class":240,"line":349},[196,499,369],{"class":262},[389,501,503],{"id":502},"configuring-the-mqtt-publisher","Configuring the MQTT Publisher",[88,505,506,515,538],{},[34,507,92,508,511,512,514],{},[56,509,510],{},"ff-mqtt-out node"," onto the canvas and connect it to the ",[56,513,107],{},". When you drag the node, it will be automatically configured with the FlowFuse MQTT broker—you do not need to manually add configuration.",[34,516,517,518,521,522,525,526,529,530,533,534,537],{},"By default, the client automatically created for your instance only has ",[56,519,520],{},"subscribe"," permissions. Click ",[56,523,524],{},"Configure Access Control"," next to the server in the node configuration window. This will redirect you to the platform’s broker client management page, filtered to show the client associated with this instance. Click the ",[56,527,528],{},"Edit"," button, enable both ",[56,531,532],{},"Publish"," and ",[56,535,536],{},"Subscribe"," actions, and then restart your instance.",[34,539,540,541,544],{},"Set the ",[56,542,543],{},"topic"," following ISA-95 hierarchy:",[231,546,551],{"className":547,"code":549,"language":550},[548],"language-text","company\u002Fsite\u002Farea\u002Fline\u002Fcell\u002Fdevice\u002Fmeasurement\n","text",[98,552,549],{"__ignoreMap":132},[10,554,555],{},"For example:",[231,557,560],{"className":558,"code":559,"language":550},[548],"acme\u002Ftokyo\u002Fassembly\u002Fline-a\u002Fpress-01\u002Ftemperature\n",[98,561,559],{"__ignoreMap":132},[88,563,564],{"start":140},[34,565,566,567,570],{},"Configure the ",[56,568,569],{},"QoS level"," in the MQTT node. Set it to QoS 1 for reliable delivery—this ensures your data reaches subscribers even if there are brief network issues, or choose according to your reliability requirements (QoS 0 for high-frequency non-critical data, QoS 2 for critical data).",[10,572,573,577],{},[129,574],{"alt":575,"dataZoomable":132,"src":576},"Configuring the MQTT Out node","\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fcsv-to-mqtt.png",[135,578,575],{},[88,580,581],{"start":278},[34,582,158],{},[73,584,586],{"id":585},"batch-processing-pipeline","Batch Processing Pipeline",[10,588,589],{},"This pipeline reads CSV files on a schedule and writes data to databases in efficient batches. Use this approach for historical data storage, trend analysis, and reporting where immediate writes aren't necessary.",[23,591,593],{"id":592},"step-1-reading-csv-files-on-schedule","Step 1: Reading CSV Files on Schedule",[10,595,596],{},"Unlike the real-time pipeline that watches for file changes, batch processing runs on a timer.",[88,598,599,617,628,634],{},[34,600,601,602,605,606],{},"Drag an ",[56,603,604],{},"Inject node"," onto the canvas. Configure it to trigger on your desired schedule:",[31,607,608,611,614],{},[34,609,610],{},"For hourly batches: Set repeat interval to \"interval\" and enter 1 hour",[34,612,613],{},"For shift-based batches: Add multiple inject nodes, For example, one at 08:00 for night shift end, another at 16:00 for day shift end, and a third at 00:00 for evening shift end.",[34,615,616],{},"For daily batches: Set to trigger \"at a specific time\" once per day",[34,618,104,619,621,622,624,625,627],{},[56,620,107],{}," and configure it to set ",[98,623,111],{}," to your CSV file path. Use a JSONata expression if you need dynamic filenames: ",[98,626,115],{},". If your files use a different naming convention, adjust the expression accordingly.",[34,629,104,630,122,632,125],{},[56,631,121],{},[98,633,111],{},[34,635,143,636,146,638,149,640,146,642,155],{},[56,637,604],{},[56,639,107],{},[56,641,107],{},[56,643,154],{},[23,645,647],{"id":646},"step-2-parsing-all-records","Step 2: Parsing All Records",[88,649,650,655],{},[34,651,104,652,654],{},[56,653,179],{}," and connect it to the File In node output.",[34,656,657,658],{},"Configure it the same way as in the real-time pipeline:",[31,659,660,662,664,666],{},[34,661,185],{},[34,663,188],{},[34,665,191],{},[34,667,194,668,199],{},[196,669,198],{},[23,671,673],{"id":672},"step-3-filtering-new-records","Step 3: Filtering New Records",[10,675,676],{},"Since we’re reading the entire CSV file each time, we need to track which rows have already been written to the database to avoid duplicates.",[88,678,679],{},[34,680,104,681,683,684,686],{},[56,682,225],{}," and connect it to the ",[56,685,179],{}," output. Add this code:",[231,688,690],{"className":233,"code":689,"language":235,"meta":132,"style":132},"const allRows = msg.payload;\n\n\u002F\u002F Get the last processed row count from flow context\nlet lastRowCount = flow.get('lastRowCount') || 0;\n\n\u002F\u002F Get only new rows since last processing\nconst newRows = allRows.slice(lastRowCount);\n\nif (newRows.length > 0) {\n    \u002F\u002F Update the row count for next run\n    flow.set('lastRowCount', allRows.length);\n    \n    msg.payload = newRows;\n    return msg;\n} else {\n    \u002F\u002F No new data to process\n    node.warn(\"No new records to process\");\n    return null;\n}\n",[98,691,692,703,707,712,745,749,754,772,776,791,796,816,822,832,839,848,854,870,879],{"__ignoreMap":132},[196,693,694,696,699,701],{"class":240,"line":241},[196,695,252],{"class":251},[196,697,698],{"class":255}," allRows",[196,700,259],{"class":251},[196,702,263],{"class":262},[196,704,705],{"class":240,"line":248},[196,706,270],{"emptyLinePlaceholder":269},[196,708,709],{"class":240,"line":266},[196,710,711],{"class":244},"\u002F\u002F Get the last processed row count from flow context\n",[196,713,714,717,720,722,725,729,732,735,738,741,743],{"class":240,"line":140},[196,715,716],{"class":251},"let",[196,718,719],{"class":262}," lastRowCount ",[196,721,311],{"class":251},[196,723,724],{"class":262}," flow.",[196,726,728],{"class":727},"sScJk","get",[196,730,731],{"class":262},"(",[196,733,734],{"class":423},"'lastRowCount'",[196,736,737],{"class":262},") ",[196,739,740],{"class":251},"||",[196,742,299],{"class":255},[196,744,363],{"class":262},[196,746,747],{"class":240,"line":278},[196,748,270],{"emptyLinePlaceholder":269},[196,750,751],{"class":240,"line":305},[196,752,753],{"class":244},"\u002F\u002F Get only new rows since last processing\n",[196,755,756,758,761,763,766,769],{"class":240,"line":328},[196,757,252],{"class":251},[196,759,760],{"class":255}," newRows",[196,762,259],{"class":251},[196,764,765],{"class":262}," allRows.",[196,767,768],{"class":727},"slice",[196,770,771],{"class":262},"(lastRowCount);\n",[196,773,774],{"class":240,"line":337},[196,775,270],{"emptyLinePlaceholder":269},[196,777,778,780,783,785,787,789],{"class":240,"line":349},[196,779,281],{"class":251},[196,781,782],{"class":262}," (newRows.",[196,784,293],{"class":255},[196,786,296],{"class":251},[196,788,299],{"class":255},[196,790,302],{"class":262},[196,792,793],{"class":240,"line":355},[196,794,795],{"class":244},"    \u002F\u002F Update the row count for next run\n",[196,797,798,801,804,806,808,811,813],{"class":240,"line":366},[196,799,800],{"class":262},"    flow.",[196,802,803],{"class":727},"set",[196,805,731],{"class":262},[196,807,734],{"class":423},[196,809,810],{"class":262},", allRows.",[196,812,293],{"class":255},[196,814,815],{"class":262},");\n",[196,817,819],{"class":240,"line":818},12,[196,820,821],{"class":262},"    \n",[196,823,825,827,829],{"class":240,"line":824},13,[196,826,308],{"class":262},[196,828,311],{"class":251},[196,830,831],{"class":262}," newRows;\n",[196,833,835,837],{"class":240,"line":834},14,[196,836,331],{"class":251},[196,838,334],{"class":262},[196,840,842,844,846],{"class":240,"line":841},15,[196,843,340],{"class":262},[196,845,343],{"class":251},[196,847,346],{"class":262},[196,849,851],{"class":240,"line":850},16,[196,852,853],{"class":244},"    \u002F\u002F No new data to process\n",[196,855,857,860,863,865,868],{"class":240,"line":856},17,[196,858,859],{"class":262},"    node.",[196,861,862],{"class":727},"warn",[196,864,731],{"class":262},[196,866,867],{"class":423},"\"No new records to process\"",[196,869,815],{"class":262},[196,871,873,875,877],{"class":240,"line":872},18,[196,874,331],{"class":251},[196,876,360],{"class":255},[196,878,363],{"class":262},[196,880,882],{"class":240,"line":881},19,[196,883,369],{"class":262},[23,885,887],{"id":886},"step-4-inserting-to-database","Step 4: Inserting to Database",[10,889,890],{},"Now we'll configure the database insertion to write batched data efficiently.",[389,892,894],{"id":893},"enabling-flowfuse-database","Enabling FlowFuse Database",[10,896,897,898,903],{},"If you have a Enterprise account, you can use the built-in PostgreSQL database service instead of setting up an external database. Follow the instructions in ",[38,899,902],{"href":900,"rel":901},"https:\u002F\u002Fflowfuse.com\u002Fblog\u002F2025\u002F08\u002Fgetting-started-with-flowfuse-tables\u002F#step-1%3A-enable-the-database-in-your-project",[42],"Getting Started with FlowFuse Tables"," to enable the database in your project.",[10,905,906],{},"Once enabled, you'll have access to a fully managed PostgreSQL database that's automatically configured and ready to use with your FlowFuse flows.",[389,908,910],{"id":909},"creating-the-database-table","Creating the Database Table",[10,912,913],{},"Before inserting data, you need to create a table to store your sensor readings.",[88,915,916,923,930,964,970,977,987],{},[34,917,918,919,922],{},"In your Node-RED editor, drag a ",[56,920,921],{},"Query node"," onto the canvas. Just like the FlowFuse MQTT nodes, it will automatically configure itself when added to the canvas.",[34,924,925,926,929],{},"Double-click to open it and click ",[56,927,928],{},"\"Ask the FlowFuse Expert\""," at the top of the configuration dialog.",[34,931,932,933,963],{},"In the assistant prompt, enter:\n",[135,934,935,936,939,940,943,944,943,947,943,949,943,952,943,955,958,959,962],{},"“Create a new table named ",[98,937,938],{},"sensor_data"," to store sensor readings with the following columns: ",[98,941,942],{},"timestamp",", ",[98,945,946],{},"site",[98,948,240],{},[98,950,951],{},"device",[98,953,954],{},"measurement",[98,956,957],{},"value",", and ",[98,960,961],{},"unit",".”",", Modify the table name and columns as needed to match your specific data and application requirements.",[34,965,966,967,969],{},"Click the ",[56,968,928],{}," button. The assistant will generate the SQL query for you and automatically populate it in the Query field.",[34,971,601,972,974,975,101],{},[56,973,604],{}," onto the canvas, set it to trigger once, and connect it to the ",[56,976,921],{},[34,978,979,980,983,984,986],{},"Add a ",[56,981,982],{},"Debug node"," connected to the ",[56,985,921],{}," output to see the result.",[34,988,989],{},"Deploy the flow and click the Inject button to create the table.",[389,991,993],{"id":992},"preparing-data-for-batch-insert","Preparing Data for Batch Insert",[10,995,996],{},"Instead of writing each CSV row individually to the database, we'll use batch inserts for much better performance. A single transaction with many rows is far more efficient than many separate transactions.",[88,998,999,1007],{},[34,1000,104,1001,1003,1004,1006],{},[56,1002,225],{}," onto the canvas and connect it to the previous ",[56,1005,225],{}," (the one that filters new records).",[34,1008,1009],{},"Name this node \"Prepare Batch Insert\" and add the following code:",[231,1011,1013],{"className":233,"code":1012,"language":235,"meta":132,"style":132},"\u002F\u002F msg.payload contains the new rows from CSV\nconst newRows = msg.payload;\n\n\u002F\u002F Transform each row to match your database schema\nconst formattedData = newRows.map(row => ({\n    timestamp: row.timestamp,\n    site: 'tokyo_plant',\n    line: 'assembly_line_a', \n    device: 'press_01',\n    measurement: 'temperature',\n    value: row.temperature,\n    unit: 'celsius'\n}));\n\n\u002F\u002F Generate parameter placeholders for batch insert\n\u002F\u002F Each row has 7 fields, so we need 7 parameters per row\n\u002F\u002F Example: ($1,$2,$3,$4,$5,$6,$7), ($8,$9,$10,$11,$12,$13,$14), ...\nconst values = formattedData.map((_, i) => \n    `($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7})`\n).join(',');\n\n\u002F\u002F Build the SQL insert query with placeholders, modify the query according to your table name and schema.\nmsg.query = `\n    INSERT INTO sensor_data \n    (timestamp, site, line, device, measurement, value, unit)\n    VALUES ${values};\n`;\n\n\u002F\u002F Flatten the data into a single array of parameters\n\u002F\u002F Order must match the columns in the INSERT statement\nmsg.params = formattedData.flatMap(r => [\n    r.timestamp,\n    r.site,\n    r.line,\n    r.device,\n    r.measurement,\n    r.value,\n    r.unit\n]);\n\nreturn msg;\n",[98,1014,1015,1020,1030,1034,1039,1066,1071,1081,1092,1102,1112,1117,1125,1130,1134,1139,1144,1149,1182,1281,1297,1302,1308,1319,1325,1331,1343,1351,1356,1362,1368,1391,1397,1403,1409,1415,1421,1427,1433,1439,1444],{"__ignoreMap":132},[196,1016,1017],{"class":240,"line":241},[196,1018,1019],{"class":244},"\u002F\u002F msg.payload contains the new rows from CSV\n",[196,1021,1022,1024,1026,1028],{"class":240,"line":248},[196,1023,252],{"class":251},[196,1025,760],{"class":255},[196,1027,259],{"class":251},[196,1029,263],{"class":262},[196,1031,1032],{"class":240,"line":266},[196,1033,270],{"emptyLinePlaceholder":269},[196,1035,1036],{"class":240,"line":140},[196,1037,1038],{"class":244},"\u002F\u002F Transform each row to match your database schema\n",[196,1040,1041,1043,1046,1048,1051,1054,1056,1060,1063],{"class":240,"line":278},[196,1042,252],{"class":251},[196,1044,1045],{"class":255}," formattedData",[196,1047,259],{"class":251},[196,1049,1050],{"class":262}," newRows.",[196,1052,1053],{"class":727},"map",[196,1055,731],{"class":262},[196,1057,1059],{"class":1058},"s4XuR","row",[196,1061,1062],{"class":251}," =>",[196,1064,1065],{"class":262}," ({\n",[196,1067,1068],{"class":240,"line":305},[196,1069,1070],{"class":262},"    timestamp: row.timestamp,\n",[196,1072,1073,1076,1079],{"class":240,"line":328},[196,1074,1075],{"class":262},"    site: ",[196,1077,1078],{"class":423},"'tokyo_plant'",[196,1080,441],{"class":262},[196,1082,1083,1086,1089],{"class":240,"line":337},[196,1084,1085],{"class":262},"    line: ",[196,1087,1088],{"class":423},"'assembly_line_a'",[196,1090,1091],{"class":262},", \n",[196,1093,1094,1097,1100],{"class":240,"line":349},[196,1095,1096],{"class":262},"    device: ",[196,1098,1099],{"class":423},"'press_01'",[196,1101,441],{"class":262},[196,1103,1104,1107,1110],{"class":240,"line":355},[196,1105,1106],{"class":262},"    measurement: ",[196,1108,1109],{"class":423},"'temperature'",[196,1111,441],{"class":262},[196,1113,1114],{"class":240,"line":366},[196,1115,1116],{"class":262},"    value: row.temperature,\n",[196,1118,1119,1122],{"class":240,"line":818},[196,1120,1121],{"class":262},"    unit: ",[196,1123,1124],{"class":423},"'celsius'\n",[196,1126,1127],{"class":240,"line":824},[196,1128,1129],{"class":262},"}));\n",[196,1131,1132],{"class":240,"line":834},[196,1133,270],{"emptyLinePlaceholder":269},[196,1135,1136],{"class":240,"line":841},[196,1137,1138],{"class":244},"\u002F\u002F Generate parameter placeholders for batch insert\n",[196,1140,1141],{"class":240,"line":850},[196,1142,1143],{"class":244},"\u002F\u002F Each row has 7 fields, so we need 7 parameters per row\n",[196,1145,1146],{"class":240,"line":856},[196,1147,1148],{"class":244},"\u002F\u002F Example: ($1,$2,$3,$4,$5,$6,$7), ($8,$9,$10,$11,$12,$13,$14), ...\n",[196,1150,1151,1153,1156,1158,1161,1163,1166,1169,1171,1174,1176,1179],{"class":240,"line":872},[196,1152,252],{"class":251},[196,1154,1155],{"class":255}," values",[196,1157,259],{"class":251},[196,1159,1160],{"class":262}," formattedData.",[196,1162,1053],{"class":727},[196,1164,1165],{"class":262},"((",[196,1167,1168],{"class":1058},"_",[196,1170,943],{"class":262},[196,1172,1173],{"class":1058},"i",[196,1175,737],{"class":262},[196,1177,1178],{"class":251},"=>",[196,1180,1181],{"class":262}," \n",[196,1183,1184,1187,1189,1192,1195,1198,1200,1203,1205,1207,1209,1211,1214,1216,1218,1220,1222,1224,1227,1229,1231,1233,1235,1237,1240,1242,1244,1246,1248,1250,1253,1255,1257,1259,1261,1263,1266,1268,1270,1272,1274,1276,1278],{"class":240,"line":881},[196,1185,1186],{"class":423},"    `($${",[196,1188,1173],{"class":262},[196,1190,1191],{"class":251}," *",[196,1193,1194],{"class":255}," 7",[196,1196,1197],{"class":251}," +",[196,1199,322],{"class":255},[196,1201,1202],{"class":423},"}, $${",[196,1204,1173],{"class":262},[196,1206,1191],{"class":251},[196,1208,1194],{"class":255},[196,1210,1197],{"class":251},[196,1212,1213],{"class":255}," 2",[196,1215,1202],{"class":423},[196,1217,1173],{"class":262},[196,1219,1191],{"class":251},[196,1221,1194],{"class":255},[196,1223,1197],{"class":251},[196,1225,1226],{"class":255}," 3",[196,1228,1202],{"class":423},[196,1230,1173],{"class":262},[196,1232,1191],{"class":251},[196,1234,1194],{"class":255},[196,1236,1197],{"class":251},[196,1238,1239],{"class":255}," 4",[196,1241,1202],{"class":423},[196,1243,1173],{"class":262},[196,1245,1191],{"class":251},[196,1247,1194],{"class":255},[196,1249,1197],{"class":251},[196,1251,1252],{"class":255}," 5",[196,1254,1202],{"class":423},[196,1256,1173],{"class":262},[196,1258,1191],{"class":251},[196,1260,1194],{"class":255},[196,1262,1197],{"class":251},[196,1264,1265],{"class":255}," 6",[196,1267,1202],{"class":423},[196,1269,1173],{"class":262},[196,1271,1191],{"class":251},[196,1273,1194],{"class":255},[196,1275,1197],{"class":251},[196,1277,1194],{"class":255},[196,1279,1280],{"class":423},"})`\n",[196,1282,1284,1287,1290,1292,1295],{"class":240,"line":1283},20,[196,1285,1286],{"class":262},").",[196,1288,1289],{"class":727},"join",[196,1291,731],{"class":262},[196,1293,1294],{"class":423},"','",[196,1296,815],{"class":262},[196,1298,1300],{"class":240,"line":1299},21,[196,1301,270],{"emptyLinePlaceholder":269},[196,1303,1305],{"class":240,"line":1304},22,[196,1306,1307],{"class":244},"\u002F\u002F Build the SQL insert query with placeholders, modify the query according to your table name and schema.\n",[196,1309,1311,1314,1316],{"class":240,"line":1310},23,[196,1312,1313],{"class":262},"msg.query ",[196,1315,311],{"class":251},[196,1317,1318],{"class":423}," `\n",[196,1320,1322],{"class":240,"line":1321},24,[196,1323,1324],{"class":423},"    INSERT INTO sensor_data \n",[196,1326,1328],{"class":240,"line":1327},25,[196,1329,1330],{"class":423},"    (timestamp, site, line, device, measurement, value, unit)\n",[196,1332,1334,1337,1340],{"class":240,"line":1333},26,[196,1335,1336],{"class":423},"    VALUES ${",[196,1338,1339],{"class":262},"values",[196,1341,1342],{"class":423},"};\n",[196,1344,1346,1349],{"class":240,"line":1345},27,[196,1347,1348],{"class":423},"`",[196,1350,363],{"class":262},[196,1352,1354],{"class":240,"line":1353},28,[196,1355,270],{"emptyLinePlaceholder":269},[196,1357,1359],{"class":240,"line":1358},29,[196,1360,1361],{"class":244},"\u002F\u002F Flatten the data into a single array of parameters\n",[196,1363,1365],{"class":240,"line":1364},30,[196,1366,1367],{"class":244},"\u002F\u002F Order must match the columns in the INSERT statement\n",[196,1369,1371,1374,1376,1378,1381,1383,1386,1388],{"class":240,"line":1370},31,[196,1372,1373],{"class":262},"msg.params ",[196,1375,311],{"class":251},[196,1377,1160],{"class":262},[196,1379,1380],{"class":727},"flatMap",[196,1382,731],{"class":262},[196,1384,1385],{"class":1058},"r",[196,1387,1062],{"class":251},[196,1389,1390],{"class":262}," [\n",[196,1392,1394],{"class":240,"line":1393},32,[196,1395,1396],{"class":262},"    r.timestamp,\n",[196,1398,1400],{"class":240,"line":1399},33,[196,1401,1402],{"class":262},"    r.site,\n",[196,1404,1406],{"class":240,"line":1405},34,[196,1407,1408],{"class":262},"    r.line,\n",[196,1410,1412],{"class":240,"line":1411},35,[196,1413,1414],{"class":262},"    r.device,\n",[196,1416,1418],{"class":240,"line":1417},36,[196,1419,1420],{"class":262},"    r.measurement,\n",[196,1422,1424],{"class":240,"line":1423},37,[196,1425,1426],{"class":262},"    r.value,\n",[196,1428,1430],{"class":240,"line":1429},38,[196,1431,1432],{"class":262},"    r.unit\n",[196,1434,1436],{"class":240,"line":1435},39,[196,1437,1438],{"class":262},"]);\n",[196,1440,1442],{"class":240,"line":1441},40,[196,1443,270],{"emptyLinePlaceholder":269},[196,1445,1447,1450],{"class":240,"line":1446},41,[196,1448,1449],{"class":251},"return",[196,1451,334],{"class":262},[88,1453,1454],{"start":266},[34,1455,1456],{},"Click Done.",[10,1458,1459],{},"This function generates a parameterized SQL query that inserts all rows in a single database transaction, which is significantly faster than individual inserts.",[73,1461,1463],{"id":1462},"visualizing-csv-data-on-dashboard","Visualizing CSV Data on Dashboard",[10,1465,1466,1467],{},"Now that we've ingested CSV data into MQTT and databases, let's visualize it on a dashboard. We'll focus on real-time visualization using the MQTT pipeline we built earlier. For historical data visualization from FlowFuse Tables, refer to our comprehensive guide: ",[38,1468,1470],{"href":1469},"\u002Fblog\u002F2025\u002F08\u002Ftime-series-dashboard-flowfuse-postgresql\u002F","Building Time-Series Dashboards with FlowFuse Tables",[389,1472,1474],{"id":1473},"step-1-installing-dashboard","Step 1: Installing Dashboard",[88,1476,1477,1480,1486,1493,1499],{},[34,1478,1479],{},"In your Node-RED editor, click the hamburger menu (≡) in the top right corner.",[34,1481,1482,1483,101],{},"Select ",[56,1484,1485],{},"Manage palette",[34,1487,1488,1489,1492],{},"Go to the ",[56,1490,1491],{},"Install"," tab.",[34,1494,1495,1496,101],{},"Search for ",[98,1497,1498],{},"@flowfuse\u002Fnode-red-dashboard",[34,1500,1501,1502,1504],{},"Click ",[56,1503,1491],{}," and confirm.",[389,1506,1508],{"id":1507},"step-2-subscribing-to-mqtt-data","Step 2: Subscribing to MQTT Data",[88,1510,1511,1517,1523],{},[34,1512,601,1513,1516],{},[56,1514,1515],{},"ff-mqtt-in node"," onto the canvas.",[34,1518,1519,1520],{},"Configure the Topic to match your publisher: ",[98,1521,1522],{},"acme\u002Ftokyo\u002Fassembly\u002Fline-a\u002Fpress-01\u002Ftemperature",[34,1524,1525,1526,1529],{},"Set ",[56,1527,1528],{},"Output"," to \"auto-detect (string or buffer)\".",[10,1531,1532,1535],{},[129,1533],{"alt":575,"dataZoomable":132,"src":1534},"\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fmqtt-to-dashboard.png",[135,1536,575],{},[389,1538,1540],{"id":1539},"step-3-creating-the-dashboard-widgets","Step 3: Creating the Dashboard Widgets",[10,1542,1543],{},"Now we'll add a chart to visualize the temperature data in real-time.",[88,1545,1546,1552,1555],{},[34,1547,104,1548,1551],{},[56,1549,1550],{},"ui-chart"," widget onto the canvas and double-click it. Create a new UI group for it to render.",[34,1553,1554],{},"Set the size according to your needs for the chart, label to \"Temperature\", type to \"line\", action to \"append\", and x-axis to \"timescale\".",[34,1556,1557,1558,1561,1562,1565,1566,101],{},"Next, set series to ",[98,1559,1560],{},"msg.payload.device",", x to ",[98,1563,1564],{},"msg.payload.timestamp",", and y to ",[98,1567,1568],{},"msg.payload.value",[10,1570,1571,1575],{},[129,1572],{"alt":1573,"dataZoomable":132,"src":1574},"UI Chart configuration showing line chart type, append action, timescale x-axis, with series, x, and y values mapped to payload properties","\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fchart-node-config.png",[135,1576,1577],{},"Chart widget configuration for real-time temperature visualization",[88,1579,1580],{"start":140},[34,1581,1501,1582,101],{},[56,1583,1584],{},"Deploy",[10,1586,1587],{},"Once deployed, open the dashboard — you should see a real-time line chart displaying temperature values over time, with each device shown as a separate series. Data points will automatically update as new MQTT messages arrive.",[10,1589,1590,1594],{},[129,1591],{"alt":1592,"dataZoomable":132,"src":1593},"Dashboard showing real-time line chart with temperature data updating as new MQTT messages arrive","\u002Fblog\u002F2025\u002F11\u002Fimages\u002Fflowfuse-dashboard.gif",[135,1595,1596],{},"Live dashboard displaying real-time temperature readings from CSV data stream",[73,1598,1600],{"id":1599},"whats-next","What's Next",[10,1602,1603],{},"You've transformed static CSV files into live data streams that flow through MQTT, databases, and dashboards. Your legacy equipment now communicates with modern systems without any hardware changes.",[10,1605,1606],{},"This tutorial walked you through building a data pipeline. FlowFuse takes it further by handling both development and production deployment. When you need to scale across multiple production lines, manage team collaboration, deploy to hundreds of devices remotely, or maintain infrastructure like MQTT brokers and databases, FlowFuse provides the platform to do it all. As you saw, it also includes an FlowFuse Expert to speed up your workflow.",[10,1608,1609,1610,1614],{},"If you want to see more on how FlowFuse helps with scaling and production deployments, ",[38,1611,1613],{"href":1612},"\u002Fbook-demo\u002F","book a demo"," today.",[1616,1617,1618],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":132,"searchDepth":248,"depth":248,"links":1620},[1621,1622,1623,1629,1635,1636],{"id":25,"depth":266,"text":26},{"id":62,"depth":266,"text":63},{"id":75,"depth":248,"text":76,"children":1624},[1625,1626,1627,1628],{"id":82,"depth":266,"text":83},{"id":168,"depth":266,"text":169},{"id":214,"depth":266,"text":215},{"id":379,"depth":266,"text":380},{"id":585,"depth":248,"text":586,"children":1630},[1631,1632,1633,1634],{"id":592,"depth":266,"text":593},{"id":646,"depth":266,"text":647},{"id":672,"depth":266,"text":673},{"id":886,"depth":266,"text":887},{"id":1462,"depth":248,"text":1463},{"id":1599,"depth":248,"text":1600},"md",{"navTitle":5,"excerpt":1639},{"type":7,"value":1640},[1641],[10,1642,12],{},"\u002Fblog\u002F2025\u002F11\u002Fcsv-mqtt-database-dashboard-flowfuse",{"title":5,"description":12},"blog\u002F2025\u002F11\u002Fcsv-mqtt-database-dashboard-flowfuse","3Q4MTdbwnlEc8ocH6ReoVJUhpTEr0vGddsIiGBMT2AA",1780070553719]