Skip to content

Commit b6aa55b

Browse files
committed
tutorials: Modernize the Firmata device Python tutorial for Syntalos 2.0
1 parent 4cb71bf commit b6aa55b

File tree

1 file changed

+82
-86
lines changed

1 file changed

+82
-86
lines changed

content/tutorials/04_firmata-interface.md

Lines changed: 82 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,28 @@ You can also add an output port of type `TableRow` named `table-out` for later u
3737
The we create some boilerplate code for the Python module, which does nothing, for now:
3838

3939
```python
40-
import syio as sy
41-
from syio import InputWaitResult
40+
import syntalos_mlink as syl
41+
4242

4343
fm_iport = sy.get_input_port('firmata-in')
4444
fm_oport = sy.get_input_port('firmatactl-out')
4545
tab_oport = sy.get_input_port('table-out')
4646

4747

48-
def prepare():
49-
pass
48+
def prepare() -> bool:
49+
"""This function is called before a run is started.
50+
You can use it for (slow) initializations."""
51+
return True
5052

5153

5254
def start():
55+
"""This function is called immediately when a run is started.
56+
This function should complete extremely quickly."""
5357
pass
5458

5559

56-
def loop() -> bool:
57-
# wait for new input to arrive
58-
wait_result = sy.await_new_input()
59-
if wait_result == InputWaitResult.CANCELLED:
60-
return False
61-
62-
# return True, so the loop function is called again when new data is available
63-
return True
64-
65-
6660
def stop():
61+
"""This function is called once a run is stopped."""
6762
pass
6863
```
6964

@@ -113,47 +108,50 @@ when we sent the command to get the LED to blink.
113108

114109
This is the code we need to achieve that:
115110

116-
```python {linenos=table,hl_lines=[11,17,23,31]}
117-
import syio as sy
118-
from syio import InputWaitResult, ControlCommand, ControlCommandKind
111+
```python {linenos=table,hl_lines=[10,16,24,33]}
112+
import syntalos_mlink as syl
119113

120114

121115
# constants
122116
LED_DURATION_MSEC = 250
123117
LED_INTERVAL_MSEC = 2000
124118

125119

126-
fm_iport = sy.get_input_port('firmata-in')
127-
fm_oport = sy.get_input_port('firmatactl-out')
128-
tab_oport = sy.get_input_port('table-out')
120+
fm_iport = syl.get_input_port('firmata-in')
121+
fm_oport = syl.get_input_port('firmatactl-out')
122+
tab_oport = syl.get_input_port('table-out')
129123

130124

131-
def prepare():
125+
def prepare() -> bool:
132126
# set table header and save filename
133127
tab_oport.set_metadata_value('table_header', ['Time', 'Event'])
134128
tab_oport.set_metadata_value('data_name_proposal', 'events/led_status')
135129

130+
return True
131+
136132

137133
def start():
138134
# set pin 8 as LED output pin
139135
fm_oport.firmata_register_digital_pin(8, 'led1', True)
140136

137+
# start sending our pulse command periodically
138+
trigger_led_pulse()
141139

142-
def loop() -> bool:
143-
# loop forever, as we do not need to read any input data
144-
while True:
145-
tab_oport.submit([sy.time_since_start_msec(),
146-
'led-pulse'])
147-
fm_oport.firmata_submit_digital_pulse('led1', LED_DURATION_MSEC)
148140

149-
sy.wait(LED_INTERVAL_MSEC)
150-
if not sy.check_running():
151-
break
141+
def trigger_led_pulse():
142+
tab_oport.submit([syl.time_since_start_msec(),
143+
'led-pulse'])
144+
fm_oport.firmata_submit_digital_pulse('led1', LED_DURATION_MSEC)
152145

153-
# ensure LED is off
154-
fm_oport.firmata_submit_digital_value('led1', False)
146+
if not syl.is_running():
147+
return False
155148

156-
return False
149+
# run this function again after some delay
150+
syl.schedule_delayed_call(LED_INTERVAL_MSEC, send_beep)
151+
152+
def stop():
153+
# ensure LED is off once we stop
154+
fm_oport.firmata_submit_digital_value('led1', False)
157155
```
158156

159157
Initially, in line 10, we need to fetch references to our input/output ports (using only the latter for now), so we
@@ -168,28 +166,30 @@ we first register pin `8` on the Arduino as digital output pin (adjust this if y
168166
This example uses convenience methods to handle digital pins. For example, the call to
169167
`firmata_register_digital_pin()` on the Firmata control port could also be written as:
170168
```python
171-
ctl = sy.new_firmatactl_with_id_name(sy.FirmataCommandKind.NEW_DIG_PIN, 8, 'led1')
169+
ctl = syl.new_firmatactl_with_id_name(syl.FirmataCommandKind.NEW_DIG_PIN, 8, 'led1')
172170
ctl.is_output = True
173171
fm_oport.submit(ctl)
174172
```
175173
Not every action has convenience methods, but the most common operations do.
176174
{{< /callout >}}
177175

178-
Then, in the `loop()` function the actual logic happens to make the LED blink. Normally, this function is called
179-
by Syntalos constantly when new data arrives. But since we do not need to wait for incoming data, we first just enter
180-
an endless `while` loop.
181-
In it, we send a new table row to the *Table* module for storage & display, using the `sy.time_since_start_msec()` function
176+
Then, we launch our custom function `trigger_led_pulse()` where the actual logic happens to make the LED blink.
177+
In it, we send a new table row to the *Table* module for storage & display, using the `syl.time_since_start_msec()` function
182178
to get the current time since the experiment run was started and naming the event `led-pulse`. You should see these two values
183179
show up in the table later. Then, we actually send a message to the *Firmata IO* module to instruct it to set the LED pin `HIGH`
184-
for the time `LED_DURATION_MSEC`. Then we wait using `sy.wait(LED_INTERVAL_MSEC)` until we repeat the process again, and exit
185-
the loop when the experiment is stopped.
180+
for the time `LED_DURATION_MSEC`.
181+
182+
To introduce some delay before sending another such command, we instruct the `trigger_led_pulse()` function to be called again
183+
after `LED_INTERVAL_MSEC` via `syl.schedule_delayed_call(LED_INTERVAL_MSEC, send_beep)`.
184+
This is repeated until the experiment has been stopped by the user.
186185

187186
{{< callout type="warning" >}}
188187
Keep in mid that when submitting data on a port, you are **not** calling the respective task immediately - you are
189188
merely enqueueing an instructions for the other module to act upon at a later time.
190189
Realistically, Syntalos will execute the queued action instantly with little delay, but Syntalos can not make any
191-
real-time guarantees. If you need those, consider using dedicated hardware or an FPGA, and control those components
192-
with Syntalos instead.
190+
real-time guarantees for inter-module communication.
191+
If you need those, consider using dedicated hardware or an FPGA, and control those components with Syntalos instead.
192+
This will give you predictable and reliable latencies.
193193
{{< /callout >}}
194194

195195
If you hit the *Run* button, the experiment should run and the LED should blink for 250 msec every 2 sec.
@@ -201,25 +201,29 @@ We assume you have a switch placed on one Ardino pin, and an LED on another for
201201

202202
The code we need for this looks very similar to our previous one:
203203

204-
```python {linenos=table,hl_lines=[22,30,36,47]}
205-
import syio as sy
206-
from syio import InputWaitResult, ControlCommand, ControlCommandKind
204+
```python {linenos=table,hl_lines=[19,26,40]}
205+
import syntalos_mlink as syl
207206

208207

209208
# constants
210209
LED_DURATION_MSEC = 500
211210

212211

213-
fm_iport = sy.get_input_port('firmata-in')
214-
fm_oport = sy.get_input_port('firmatactl-out')
215-
tab_oport = sy.get_input_port('table-out')
212+
fm_iport = syl.get_input_port('firmata-in')
213+
fm_oport = syl.get_input_port('firmatactl-out')
214+
tab_oport = syl.get_input_port('table-out')
216215

217216

218-
def prepare():
217+
def prepare() -> bool:
219218
# set table header and save filename
220219
tab_oport.set_metadata_value('table_header', ['Time', 'Event'])
221220
tab_oport.set_metadata_value('data_name_proposal', 'events/led_status')
222221

222+
# call a function once new data was received on this input port
223+
fm_iport.on_data = on_new_firmata_data
224+
225+
return True
226+
223227

224228
def start():
225229
# set pin 7 as input pin
@@ -229,50 +233,42 @@ def start():
229233
fm_oport.firmata_register_digital_pin(8, 'led1', True)
230234

231235

232-
def loop() -> bool:
233-
# wait for new input to arrive
234-
if sy.await_new_input() == InputWaitResult.CANCELLED:
235-
# the run has been cancelled (by the user or an error)
236-
return False
236+
def on_new_firmata_data(data):
237+
if data is None:
238+
return
237239

238-
while True:
239-
data = fm_iport.next()
240-
if data is None:
241-
# no more data, exit
242-
break
243-
244-
# we are only interested in digital input
245-
if not data.is_digital:
246-
continue
247-
# we only want to look at the 'switch' pin
248-
if data.pin_name != 'switch':
249-
continue
250-
251-
if data.value:
252-
tab_oport.submit([sy.time_since_start_msec(),
253-
'switch-on'])
254-
fm_oport.firmata_submit_digital_pulse('led1', LED_DURATION_MSEC)
255-
else:
256-
tab_oport.submit([sy.time_since_start_msec(),
257-
'switch-off'])
258-
259-
# return True, so this function is called again
260-
return True
240+
# we are only interested in digital input
241+
if not data.is_digital:
242+
return
243+
# we only want to look at the 'switch' pin
244+
if data.pin_name != 'switch':
245+
return
246+
247+
if data.value:
248+
tab_oport.submit([syl.time_since_start_msec(),
249+
'switch-on'])
250+
fm_oport.firmata_submit_digital_pulse('led1', LED_DURATION_MSEC)
251+
else:
252+
tab_oport.submit([syl.time_since_start_msec(),
253+
'switch-off'])
254+
255+
256+
def stop():
257+
# ensure LED is off once we stop
258+
fm_oport.firmata_submit_digital_value('led1', False)
261259
```
262260

263-
In `start()` we additionally register pin `7` as an input pin this time, while all the other changes are in the `loop()`
264-
function. There, we initially just wait for new input to arrive. The `sy.await_new_input()` call will return if there was
265-
new data to process on *any* of the Python script modules' input ports. In this case we have only one input port, but of we
266-
had more than one we would now need to check all input ports for new data. Since there might also be more than one data block,
267-
we enter a `while` loop and pull new data from the input port using `fm_iport.next()` until no more data is available.
261+
In `start()` we now additionally register pin `7` as an input pin this time.
262+
We also need to read input from our Firmata device now, for which we registered `fm_iport` as input port.
263+
Every input port in Syntalos' Python interface has a `on_data` property which you can assign a custom function to,
264+
to be called when new data is received. We assign our own `on_new_firmata_data()` function in this example.
268265

269-
Next, we check if we have data from the right, registered block by checking if the pin is digital and if it is our `switch` labelled
270-
pin. We ignore any other data (there should not be any, but just in case...).
266+
In `on_new_firmata_data()`, we first check if the received `data` is valid (it may be `None` to signal that a run
267+
is being stopped right now). Next, we check if we have data from the right pin by checking if it is is digital and
268+
if it is the pin that we previously labelled "`switch`". We ignore any other data (there should not be any, but just in case...).
271269
Then, if we receive a `True` value, we command the LED to blink for half a second and log that fact in our table, otherwise
272270
we just log the fact that the switch is off.
273271

274-
Finally, we let the `loop()` function return `True`, so it is called again soon.
275-
276272
Upon running this project, you should see the LED flash briefly once you push the button, and see the state of the button logged
277273
in the table displayed by the *Table* module.
278274

0 commit comments

Comments
 (0)