Skip to content

Commit 20a9e0a

Browse files
authored
Update system time as part of starting image acquisition (#590)
This PR resolves #507. TODOs: - [x] Try to achieve a single-button-press UX - [x] Implement a ridiculous workaround (20-second delay between changing system time and starting image acquisition) for eclipse-mosquitto/mosquitto#2205 + node-red/node-red#2864 (Node-RED's MQTT client disconnects for ~15 sec when system time changes) - [x] Try to reduce the MQTT reconnection interval for Node-RED from 15 sec to 1 sec - [x] Update the planktoscopehat dashboard to match changes in the adafruithat dashboard - [x] Update the changelog
1 parent 4f5ace4 commit 20a9e0a

File tree

4 files changed

+649
-523
lines changed

4 files changed

+649
-523
lines changed

software/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All dates in this file are given in the [UTC time zone](https://en.wikipedia.org
1818

1919
### Changed
2020

21+
- (Breaking change; Application: GUI) When you press the "Start Acquisition" button in the Node-RED dashboard, it now automatically adjusts the PlanktoScope's system time to match the time in your web browser (but still in UTC time zone on the PlanktoScope) if the times are different by more than one minute, so that datasets will be created with correct dates in the dataset directory structure.
2122
- (Breaking change; System) The official PlanktoScope OS images are now built on Raspberry Pi OS 12 (bookworm) instead of Raspberry Pi OS 11 (bullseye). No support will be given for running the PlanktoScope OS setup scripts on bullseye base images, and the OS setup scripts will not work on bullseye.
2223
- (Application: GUI) The Node-RED dashboard now initializes the Sample page's Dilution Factor field to 1.0, instead of leaving it empty.
2324
- (System: networking) Wi-Fi hotspot behavior and network connection management is now based on NetworkManager, as part of an upgrade to Raspberry Pi OS 12 (bookworm). As part of this change the previous autohotspot service has been removed, as it's redundant with functionality now provided by NetworkManager.

software/node-red-dashboard/adafruithat/flows.json

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4872,32 +4872,6 @@
48724872
[]
48734873
]
48744874
},
4875-
{
4876-
"id": "4b489713.ccde5",
4877-
"type": "ui_button",
4878-
"z": "baa1e3d9.cb29d",
4879-
"name": "",
4880-
"group": "4322c187.e73e5",
4881-
"order": 12,
4882-
"width": 5,
4883-
"height": 1,
4884-
"passthru": false,
4885-
"label": "Start Acquisition",
4886-
"tooltip": "",
4887-
"color": "",
4888-
"bgcolor": "",
4889-
"icon": "camera",
4890-
"payload": "",
4891-
"payloadType": "str",
4892-
"topic": "imager/image",
4893-
"x": 460,
4894-
"y": 480,
4895-
"wires": [
4896-
[
4897-
"c9f510c0.7d1328"
4898-
]
4899-
]
4900-
},
49014875
{
49024876
"id": "c9f510c0.7d1328",
49034877
"type": "function",
@@ -4935,6 +4909,7 @@
49354909
"ok": "OK",
49364910
"cancel": "",
49374911
"raw": false,
4912+
"className": "",
49384913
"topic": "",
49394914
"name": "",
49404915
"x": 950,
@@ -4951,6 +4926,11 @@
49514926
"topic": "",
49524927
"qos": "",
49534928
"retain": "",
4929+
"respTopic": "",
4930+
"contentType": "",
4931+
"userProps": "",
4932+
"correl": "",
4933+
"expiry": "",
49544934
"broker": "8dc3722c.06efa8",
49554935
"x": 1150,
49564936
"y": 440,
@@ -4970,10 +4950,12 @@
49704950
"tooltip": "",
49714951
"color": "",
49724952
"bgcolor": "#AD1625",
4953+
"className": "",
49734954
"icon": "cancel",
49744955
"payload": "{\"action\":\"stop\"}",
49754956
"payloadType": "json",
49764957
"topic": "imager/image",
4958+
"topicType": "str",
49774959
"x": 460,
49784960
"y": 520,
49794961
"wires": [
@@ -5093,7 +5075,7 @@
50935075
"payloadType": "str",
50945076
"topic": "imager/image",
50955077
"topicType": "str",
5096-
"x": 460,
5078+
"x": 680,
50975079
"y": 440,
50985080
"wires": [
50995081
[
@@ -5844,6 +5826,29 @@
58445826
]
58455827
]
58465828
},
5829+
{
5830+
"id": "f4caa15e2bbfd6e7",
5831+
"type": "ui_template",
5832+
"z": "baa1e3d9.cb29d",
5833+
"group": "4322c187.e73e5",
5834+
"name": "Start Acquisition",
5835+
"order": 13,
5836+
"width": "5",
5837+
"height": "1",
5838+
"format": "<button class=\"md-raised md-button\" type=\"button\" ng-click=\"startAcquisition()\" style=\"z-index: 1; padding: 1em; width: 100%;\">\n <span>Start Acquisition</span>\n</button>\n\n<script>\n (function(scope) {\n scope.startAcquisition = async function() {\n const systemTime = await getSystemTime();\n if (systemTime !== undefined && isDrifted(new Date(systemTime.timestamp))) {\n await setSystemTime((new Date()).getTime());\n // Changing the system time seems to trigger a MQTT client\n // disconnection+reconnection in Node-RED, and the duration of\n // disconnection is controlled by a reconnection interval in\n // Node-RED's settings.js file:\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n scope.send({topic: 'imager/image'});\n }\n \n async function getSystemTime() {\n const response = await fetch('/ps/node-red-v2/api/system-time', {method: 'GET'});\n if (!response.ok) {\n alert(`Couldn't check whether system time is accurate (HTTP error ${response.status}); press OK to continue anyways...`);\n return undefined;\n }\n try {\n const result = await response.json();\n if (!('timestamp' in result)) {\n // TODO: emit a notification toast message instead\n alert('Couldn\\'t check whether system time is accurate (invalid JSON object in response body); press OK to continue anyways...');\n return undefined;\n }\n return result;\n } catch (e) {\n // TODO: emit a notification toast message instead\n alert('Couldn\\'t check whether system time is accurate (unable to parse response body as JSON); press OK to continue anyways...');\n return undefined;\n }\n }\n function isDrifted(systemTime) {\n const browserTime = (new Date()).getTime();\n const drift = Math.abs(browserTime - systemTime);\n const threshold = 1000 * 60; // 1 minute\n return drift >= threshold;\n }\n async function setSystemTime(timestamp) {\n const response = await fetch('/ps/node-red-v2/api/system-time', {\n method: 'POST',\n headers: {'Content-Type': 'application/json; charset=UTF-8'},\n body: JSON.stringify({timestamp: timestamp}),\n });\n if (!response.ok) {\n // TODO: emit a notification toast message instead\n alert('We might have failed to update the system time to match the time in your web browser; press OK to continue anyways...');\n return;\n }\n }\n })(scope);\n</script>",
5839+
"storeOutMessages": false,
5840+
"fwdInMessages": false,
5841+
"resendOnRefresh": false,
5842+
"templateScope": "local",
5843+
"className": "",
5844+
"x": 460,
5845+
"y": 480,
5846+
"wires": [
5847+
[
5848+
"c9f510c0.7d1328"
5849+
]
5850+
]
5851+
},
58475852
{
58485853
"id": "9d6abe67.6bb3d",
58495854
"type": "ui_button",
@@ -8230,7 +8235,8 @@
82308235
"wires": [
82318236
[
82328237
"19a45f972be2b0f1",
8233-
"07bbdca8d6153210"
8238+
"07bbdca8d6153210",
8239+
"595df18543c8328e"
82348240
]
82358241
]
82368242
},
@@ -8292,6 +8298,67 @@
82928298
[]
82938299
]
82948300
},
8301+
{
8302+
"id": "c8be8dad272e1fa5",
8303+
"type": "http in",
8304+
"z": "1371dec5.76e671",
8305+
"name": "GET /api/system-time",
8306+
"url": "/api/system-time",
8307+
"method": "get",
8308+
"upload": false,
8309+
"swaggerDoc": "",
8310+
"x": 120,
8311+
"y": 420,
8312+
"wires": [
8313+
[
8314+
"0f4ce59587fa14e1"
8315+
]
8316+
]
8317+
},
8318+
{
8319+
"id": "0f4ce59587fa14e1",
8320+
"type": "function",
8321+
"z": "1371dec5.76e671",
8322+
"name": "Get system time",
8323+
"func": "var date = new Date();\nmsg.payload = {'timestamp': date.getTime()};\nreturn msg",
8324+
"outputs": 1,
8325+
"timeout": 0,
8326+
"noerr": 0,
8327+
"initialize": "",
8328+
"finalize": "",
8329+
"libs": [],
8330+
"x": 340,
8331+
"y": 420,
8332+
"wires": [
8333+
[
8334+
"433d48a4b1bc0b27"
8335+
]
8336+
]
8337+
},
8338+
{
8339+
"id": "433d48a4b1bc0b27",
8340+
"type": "http response",
8341+
"z": "1371dec5.76e671",
8342+
"name": "Send GET response",
8343+
"statusCode": "",
8344+
"headers": {
8345+
"Content-Type": "application/json; charset=UTF-8"
8346+
},
8347+
"x": 620,
8348+
"y": 420,
8349+
"wires": []
8350+
},
8351+
{
8352+
"id": "595df18543c8328e",
8353+
"type": "http response",
8354+
"z": "1371dec5.76e671",
8355+
"name": "Send POST response",
8356+
"statusCode": "",
8357+
"headers": {},
8358+
"x": 620,
8359+
"y": 300,
8360+
"wires": []
8361+
},
82958362
{
82968363
"id": "1fdf77b5.24e4e",
82978364
"type": "mqtt in",

software/node-red-dashboard/planktoscopehat/flows.json

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5066,32 +5066,6 @@
50665066
[]
50675067
]
50685068
},
5069-
{
5070-
"id": "4b489713.ccde5",
5071-
"type": "ui_button",
5072-
"z": "baa1e3d9.cb29d",
5073-
"name": "",
5074-
"group": "4322c187.e73e5",
5075-
"order": 12,
5076-
"width": 5,
5077-
"height": 1,
5078-
"passthru": false,
5079-
"label": "Start Acquisition",
5080-
"tooltip": "",
5081-
"color": "",
5082-
"bgcolor": "",
5083-
"icon": "camera",
5084-
"payload": "",
5085-
"payloadType": "str",
5086-
"topic": "imager/image",
5087-
"x": 420,
5088-
"y": 440,
5089-
"wires": [
5090-
[
5091-
"c9f510c0.7d1328"
5092-
]
5093-
]
5094-
},
50955069
{
50965070
"id": "c9f510c0.7d1328",
50975071
"type": "function",
@@ -6015,6 +5989,29 @@
60155989
]
60165990
]
60175991
},
5992+
{
5993+
"id": "37116281279b9b82",
5994+
"type": "ui_template",
5995+
"z": "baa1e3d9.cb29d",
5996+
"group": "4322c187.e73e5",
5997+
"name": "Start Acquisition",
5998+
"order": 12,
5999+
"width": "5",
6000+
"height": "1",
6001+
"format": "<button class=\"md-raised md-button\" type=\"button\" ng-click=\"startAcquisition()\" style=\"z-index: 1; padding: 1em; width: 100%;\">\n <span>Start Acquisition</span>\n</button>\n\n<script>\n (function(scope) {\n scope.startAcquisition = async function() {\n const systemTime = await getSystemTime();\n if (systemTime !== undefined && isDrifted(new Date(systemTime.timestamp))) {\n await setSystemTime((new Date()).getTime());\n // Changing the system time seems to trigger a MQTT client\n // disconnection+reconnection in Node-RED, and the duration of\n // disconnection is controlled by a reconnection interval in\n // Node-RED's settings.js file:\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n scope.send({topic: 'imager/image'});\n }\n \n async function getSystemTime() {\n const response = await fetch('/ps/node-red-v2/api/system-time', {method: 'GET'});\n if (!response.ok) {\n alert(`Couldn't check whether system time is accurate (HTTP error ${response.status}); press OK to continue anyways...`);\n return undefined;\n }\n try {\n const result = await response.json();\n if (!('timestamp' in result)) {\n // TODO: emit a notification toast message instead\n alert('Couldn\\'t check whether system time is accurate (invalid JSON object in response body); press OK to continue anyways...');\n return undefined;\n }\n return result;\n } catch (e) {\n // TODO: emit a notification toast message instead\n alert('Couldn\\'t check whether system time is accurate (unable to parse response body as JSON); press OK to continue anyways...');\n return undefined;\n }\n }\n function isDrifted(systemTime) {\n const browserTime = (new Date()).getTime();\n const drift = Math.abs(browserTime - systemTime);\n const threshold = 1000 * 60; // 1 minute\n return drift >= threshold;\n }\n async function setSystemTime(timestamp) {\n const response = await fetch('/ps/node-red-v2/api/system-time', {\n method: 'POST',\n headers: {'Content-Type': 'application/json; charset=UTF-8'},\n body: JSON.stringify({timestamp: timestamp}),\n });\n if (!response.ok) {\n // TODO: emit a notification toast message instead\n alert('We might have failed to update the system time to match the time in your web browser; press OK to continue anyways...');\n return;\n }\n }\n })(scope);\n</script>",
6002+
"storeOutMessages": false,
6003+
"fwdInMessages": false,
6004+
"resendOnRefresh": false,
6005+
"templateScope": "local",
6006+
"className": "",
6007+
"x": 420,
6008+
"y": 440,
6009+
"wires": [
6010+
[
6011+
"c9f510c0.7d1328"
6012+
]
6013+
]
6014+
},
60186015
{
60196016
"id": "9d6abe67.6bb3d",
60206017
"type": "ui_button",
@@ -8088,7 +8085,8 @@
80888085
"y": 340,
80898086
"wires": [
80908087
[
8091-
"1c4113d4c933fd4b"
8088+
"1c4113d4c933fd4b",
8089+
"423659c62bae0524"
80928090
]
80938091
]
80948092
},
@@ -8152,6 +8150,67 @@
81528150
[]
81538151
]
81548152
},
8153+
{
8154+
"id": "3822b843fda94733",
8155+
"type": "http in",
8156+
"z": "1371dec5.76e671",
8157+
"name": "GET /api/system-time",
8158+
"url": "/api/system-time",
8159+
"method": "get",
8160+
"upload": false,
8161+
"swaggerDoc": "",
8162+
"x": 120,
8163+
"y": 420,
8164+
"wires": [
8165+
[
8166+
"5b0a65d3fe187424"
8167+
]
8168+
]
8169+
},
8170+
{
8171+
"id": "5b0a65d3fe187424",
8172+
"type": "function",
8173+
"z": "1371dec5.76e671",
8174+
"name": "Get system time",
8175+
"func": "var date = new Date();\nmsg.payload = {'timestamp': date.getTime()};\nreturn msg",
8176+
"outputs": 1,
8177+
"timeout": "",
8178+
"noerr": 0,
8179+
"initialize": "",
8180+
"finalize": "",
8181+
"libs": [],
8182+
"x": 340,
8183+
"y": 420,
8184+
"wires": [
8185+
[
8186+
"312a15407574b38e"
8187+
]
8188+
]
8189+
},
8190+
{
8191+
"id": "423659c62bae0524",
8192+
"type": "http response",
8193+
"z": "1371dec5.76e671",
8194+
"name": "Send POST response",
8195+
"statusCode": "",
8196+
"headers": {},
8197+
"x": 620,
8198+
"y": 300,
8199+
"wires": []
8200+
},
8201+
{
8202+
"id": "312a15407574b38e",
8203+
"type": "http response",
8204+
"z": "1371dec5.76e671",
8205+
"name": "Send GET response",
8206+
"statusCode": "",
8207+
"headers": {
8208+
"Content-Type": "application/json; charset=UTF-8"
8209+
},
8210+
"x": 620,
8211+
"y": 420,
8212+
"wires": []
8213+
},
81558214
{
81568215
"id": "1fdf77b5.24e4e",
81578216
"type": "mqtt in",

0 commit comments

Comments
 (0)