Skip to main content

endbasic_std/gpio/
mod.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! GPIO access functions and commands for EndBASIC.
17
18use async_trait::async_trait;
19use endbasic_core::LineCol;
20use endbasic_core::ast::{ArgSep, ExprType};
21use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
22use endbasic_core::exec::{Clearable, Error, Machine, Result, Scope};
23use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder, Symbols};
24use std::any::Any;
25use std::borrow::Cow;
26use std::cell::RefCell;
27use std::io;
28use std::rc::Rc;
29
30mod fakes;
31pub use fakes::{MockPins, NoopPins};
32
33/// Category description for all symbols provided by this module.
34const CATEGORY: &str = "Hardware interface
35EndBASIC provides features to manipulate external hardware.  These features are currently limited \
36to GPIO interaction on a Raspberry Pi and are only available when EndBASIC has explicitly been \
37built with the --features=rpi option.  Support for other busses and platforms may come later.";
38
39/// Pin identifier.
40#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
41pub struct Pin(pub u8);
42
43impl Pin {
44    /// Creates a new pin number from an EndBASIC integer value.
45    fn from_i32(i: i32, pos: LineCol) -> Result<Self> {
46        if i < 0 {
47            return Err(Error::SyntaxError(pos, format!("Pin number {} must be positive", i)));
48        }
49        if i > u8::MAX as i32 {
50            return Err(Error::SyntaxError(pos, format!("Pin number {} is too large", i)));
51        }
52        Ok(Self(i as u8))
53    }
54}
55
56/// Pin configuration, which includes mode and bias.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum PinMode {
59    /// Pin that can be read from with no bias.
60    In,
61
62    /// Pin that can be read from with its built-in pull-down resistor (if present) enabled.
63    InPullDown,
64
65    /// Pin that can be read from with its built-in pull-up resistor (if present) enabled.
66    InPullUp,
67
68    /// Pin that can be written to.
69    Out,
70}
71
72impl PinMode {
73    /// Obtains a `PinMode` from a value.
74    fn parse(s: &str, pos: LineCol) -> Result<PinMode> {
75        match s.to_ascii_uppercase().as_ref() {
76            "IN" => Ok(PinMode::In),
77            "IN-PULL-UP" => Ok(PinMode::InPullUp),
78            "IN-PULL-DOWN" => Ok(PinMode::InPullDown),
79            "OUT" => Ok(PinMode::Out),
80            s => Err(Error::SyntaxError(pos, format!("Unknown pin mode {}", s))),
81        }
82    }
83}
84
85/// Generic abstraction over a GPIO chip to back all EndBASIC commands.
86pub trait Pins {
87    /// Returns `self` as `&dyn Any` to allow downcasting to a concrete type.
88    fn as_any(&self) -> &dyn Any;
89
90    /// Returns `self` as `&mut dyn Any` to allow downcasting to a concrete type.
91    fn as_any_mut(&mut self) -> &mut dyn Any;
92
93    /// Configures the `pin` as either input or output (per `mode`).
94    ///
95    /// This lazily initialies the GPIO chip as well on the first pin setup.
96    ///
97    /// It is OK to set up a pin multiple times without calling `clear()` in-between.
98    fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
99
100    /// Resets a given `pin` to its default state.
101    fn clear(&mut self, pin: Pin) -> io::Result<()>;
102
103    /// Resets all pins to their default state.
104    fn clear_all(&mut self) -> io::Result<()>;
105
106    /// Reads the value of the given `pin`, which must have been previously setup as an input pin.
107    fn read(&mut self, pin: Pin) -> io::Result<bool>;
108
109    /// Writes `v` to the given `pin`, which must have been previously setup as an output pin.
110    fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
111}
112
113/// Resets the state of the pins in a best-effort manner.
114pub(crate) struct PinsClearable {
115    pins: Rc<RefCell<dyn Pins>>,
116}
117
118impl PinsClearable {
119    /// Creates a new clearable for `pins`.
120    pub(crate) fn new(pins: Rc<RefCell<dyn Pins>>) -> Box<Self> {
121        Box::from(Self { pins })
122    }
123}
124
125impl Clearable for PinsClearable {
126    fn reset_state(&self, _syms: &mut Symbols) {
127        let _ = self.pins.borrow_mut().clear_all();
128    }
129}
130
131/// The `GPIO_SETUP` command.
132pub struct GpioSetupCommand {
133    metadata: CallableMetadata,
134    pins: Rc<RefCell<dyn Pins>>,
135}
136
137impl GpioSetupCommand {
138    /// Creates a new instance of the command.
139    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
140        Rc::from(Self {
141            metadata: CallableMetadataBuilder::new("GPIO_SETUP")
142                .with_syntax(&[(
143                    &[
144                        SingularArgSyntax::RequiredValue(
145                            RequiredValueSyntax {
146                                name: Cow::Borrowed("pin"),
147                                vtype: ExprType::Integer,
148                            },
149                            ArgSepSyntax::Exactly(ArgSep::Long),
150                        ),
151                        SingularArgSyntax::RequiredValue(
152                            RequiredValueSyntax {
153                                name: Cow::Borrowed("mode"),
154                                vtype: ExprType::Text,
155                            },
156                            ArgSepSyntax::End,
157                        ),
158                    ],
159                    None,
160                )])
161                .with_category(CATEGORY)
162                .with_description(
163                    "Configures a GPIO pin for input or output.
164Before a GPIO pin can be used for reads or writes, it must be configured to be an input or \
165output pin.  Additionally, if pull up or pull down resistors are available and desired, these \
166must be configured upfront too.
167The mode$ has to be one of \"IN\", \"IN-PULL-DOWN\", \"IN-PULL-UP\", or \"OUT\".  These values \
168are case-insensitive.  The possibility of using the pull-down and pull-up resistors depends on \
169whether they are available in the hardware, and selecting these modes will fail if they are not.
170It is OK to reconfigure an already configured pin without clearing its state first.",
171                )
172                .build(),
173            pins,
174        })
175    }
176}
177
178#[async_trait(?Send)]
179impl Callable for GpioSetupCommand {
180    fn metadata(&self) -> &CallableMetadata {
181        &self.metadata
182    }
183
184    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
185        debug_assert_eq!(2, scope.nargs());
186        let pin = {
187            let (i, pos) = scope.pop_integer_with_pos();
188            Pin::from_i32(i, pos)?
189        };
190        let mode = {
191            let (t, pos) = scope.pop_string_with_pos();
192            PinMode::parse(&t, pos)?
193        };
194
195        self.pins.borrow_mut().setup(pin, mode).map_err(|e| scope.io_error(e))?;
196        Ok(())
197    }
198}
199
200/// The `GPIO_CLEAR` command.
201pub struct GpioClearCommand {
202    metadata: CallableMetadata,
203    pins: Rc<RefCell<dyn Pins>>,
204}
205
206impl GpioClearCommand {
207    /// Creates a new instance of the command.
208    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
209        Rc::from(Self {
210            metadata: CallableMetadataBuilder::new("GPIO_CLEAR")
211                .with_syntax(&[
212                    (&[], None),
213                    (
214                        &[SingularArgSyntax::RequiredValue(
215                            RequiredValueSyntax {
216                                name: Cow::Borrowed("pin"),
217                                vtype: ExprType::Integer,
218                            },
219                            ArgSepSyntax::End,
220                        )],
221                        None,
222                    ),
223                ])
224                .with_category(CATEGORY)
225                .with_description(
226                    "Resets the GPIO chip or a specific pin.
227If no pin% is specified, resets the state of all GPIO pins. \
228If a pin% is given, only that pin is reset.  It is OK if the given pin has never been configured \
229before.",
230                )
231                .build(),
232            pins,
233        })
234    }
235}
236
237#[async_trait(?Send)]
238impl Callable for GpioClearCommand {
239    fn metadata(&self) -> &CallableMetadata {
240        &self.metadata
241    }
242
243    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
244        if scope.nargs() == 0 {
245            self.pins.borrow_mut().clear_all().map_err(|e| scope.io_error(e))?;
246        } else {
247            debug_assert_eq!(1, scope.nargs());
248            let pin = {
249                let (i, pos) = scope.pop_integer_with_pos();
250                Pin::from_i32(i, pos)?
251            };
252
253            self.pins.borrow_mut().clear(pin).map_err(|e| scope.io_error(e))?;
254        }
255
256        Ok(())
257    }
258}
259
260/// The `GPIO_READ` function.
261pub struct GpioReadFunction {
262    metadata: CallableMetadata,
263    pins: Rc<RefCell<dyn Pins>>,
264}
265
266impl GpioReadFunction {
267    /// Creates a new instance of the function.
268    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
269        Rc::from(Self {
270            metadata: CallableMetadataBuilder::new("GPIO_READ")
271                .with_return_type(ExprType::Boolean)
272                .with_syntax(&[(
273                    &[SingularArgSyntax::RequiredValue(
274                        RequiredValueSyntax {
275                            name: Cow::Borrowed("pin"),
276                            vtype: ExprType::Integer,
277                        },
278                        ArgSepSyntax::End,
279                    )],
280                    None,
281                )])
282                .with_category(CATEGORY)
283                .with_description(
284                    "Reads the state of a GPIO pin.
285Returns FALSE to represent a low value, and TRUE to represent a high value.",
286                )
287                .build(),
288            pins,
289        })
290    }
291}
292
293#[async_trait(?Send)]
294impl Callable for GpioReadFunction {
295    fn metadata(&self) -> &CallableMetadata {
296        &self.metadata
297    }
298
299    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
300        debug_assert_eq!(1, scope.nargs());
301        let pin = {
302            let (i, pos) = scope.pop_integer_with_pos();
303            Pin::from_i32(i, pos)?
304        };
305
306        let value = self.pins.borrow_mut().read(pin).map_err(|e| scope.io_error(e))?;
307        scope.return_boolean(value)
308    }
309}
310
311/// The `GPIO_WRITE` command.
312pub struct GpioWriteCommand {
313    metadata: CallableMetadata,
314    pins: Rc<RefCell<dyn Pins>>,
315}
316
317impl GpioWriteCommand {
318    /// Creates a new instance of the command.
319    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
320        Rc::from(Self {
321            metadata: CallableMetadataBuilder::new("GPIO_WRITE")
322                .with_syntax(&[(
323                    &[
324                        SingularArgSyntax::RequiredValue(
325                            RequiredValueSyntax {
326                                name: Cow::Borrowed("pin"),
327                                vtype: ExprType::Integer,
328                            },
329                            ArgSepSyntax::Exactly(ArgSep::Long),
330                        ),
331                        SingularArgSyntax::RequiredValue(
332                            RequiredValueSyntax {
333                                name: Cow::Borrowed("value"),
334                                vtype: ExprType::Boolean,
335                            },
336                            ArgSepSyntax::End,
337                        ),
338                    ],
339                    None,
340                )])
341                .with_category(CATEGORY)
342                .with_description(
343                    "Sets the state of a GPIO pin.
344A FALSE value? sets the pin to low, and a TRUE value? sets the pin to high.",
345                )
346                .build(),
347            pins,
348        })
349    }
350}
351
352#[async_trait(?Send)]
353impl Callable for GpioWriteCommand {
354    fn metadata(&self) -> &CallableMetadata {
355        &self.metadata
356    }
357
358    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
359        debug_assert_eq!(2, scope.nargs());
360        let pin = {
361            let (i, pos) = scope.pop_integer_with_pos();
362            Pin::from_i32(i, pos)?
363        };
364        let value = scope.pop_boolean();
365
366        self.pins.borrow_mut().write(pin, value).map_err(|e| scope.io_error(e))?;
367        Ok(())
368    }
369}
370
371/// The `GPIO_MOCK_INJECT` command.
372pub struct GpioMockInjectCommand {
373    metadata: CallableMetadata,
374    pins: Rc<RefCell<dyn Pins>>,
375}
376
377impl GpioMockInjectCommand {
378    /// Creates a new instance of the command.
379    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
380        Rc::from(Self {
381            metadata: CallableMetadataBuilder::new("GPIO_MOCK_INJECT")
382                .with_syntax(&[(
383                    &[
384                        SingularArgSyntax::RequiredValue(
385                            RequiredValueSyntax {
386                                name: Cow::Borrowed("pin"),
387                                vtype: ExprType::Integer,
388                            },
389                            ArgSepSyntax::Exactly(ArgSep::Long),
390                        ),
391                        SingularArgSyntax::RequiredValue(
392                            RequiredValueSyntax {
393                                name: Cow::Borrowed("high"),
394                                vtype: ExprType::Boolean,
395                            },
396                            ArgSepSyntax::End,
397                        ),
398                    ],
399                    None,
400                )])
401                .with_category(CATEGORY)
402                .with_description(
403                    "Pre-seeds a GPIO_READ result for testing.
404This command is only available when EndBASIC is started with --gpio-pins=mock.  It pre-seeds \
405the next GPIO_READ call for the given pin% to return the given high? value.",
406                )
407                .build(),
408            pins,
409        })
410    }
411}
412
413#[async_trait(?Send)]
414impl Callable for GpioMockInjectCommand {
415    fn metadata(&self) -> &CallableMetadata {
416        &self.metadata
417    }
418
419    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
420        debug_assert_eq!(2, scope.nargs());
421        let pin = {
422            let (i, pos) = scope.pop_integer_with_pos();
423            Pin::from_i32(i, pos)?
424        };
425        let high = scope.pop_boolean();
426
427        self.pins
428            .borrow_mut()
429            .as_any_mut()
430            .downcast_mut::<MockPins>()
431            .expect("Only registered for mock backend")
432            .inject_read(pin, high);
433        Ok(())
434    }
435}
436
437/// The `GPIO_MOCK_TRACE` function.
438pub struct GpioMockTraceFunction {
439    metadata: CallableMetadata,
440    pins: Rc<RefCell<dyn Pins>>,
441}
442
443impl GpioMockTraceFunction {
444    /// Creates a new instance of the function.
445    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
446        Rc::from(Self {
447            metadata: CallableMetadataBuilder::new("GPIO_MOCK_TRACE")
448                .with_return_type(ExprType::Text)
449                .with_syntax(&[(&[], None)])
450                .with_category(CATEGORY)
451                .with_description(
452                    "Returns the GPIO operation trace for testing.
453This function is only available when EndBASIC is started with --gpio-pins=mock.  It returns a \
454space-separated list of integers representing the ordered record of all GPIO operations \
455performed since the last reset.",
456                )
457                .build(),
458            pins,
459        })
460    }
461}
462
463#[async_trait(?Send)]
464impl Callable for GpioMockTraceFunction {
465    fn metadata(&self) -> &CallableMetadata {
466        &self.metadata
467    }
468
469    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
470        debug_assert_eq!(0, scope.nargs());
471        let pins = self.pins.borrow();
472        let mock =
473            pins.as_any().downcast_ref::<MockPins>().expect("Only registered for mock backend");
474        let result = mock.trace().iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
475        scope.return_string(result)
476    }
477}
478
479/// Adds all symbols provided by this module to the given `machine`.
480pub fn add_all(machine: &mut Machine, pins: Rc<RefCell<dyn Pins>>) {
481    if pins.borrow().as_any().downcast_ref::<MockPins>().is_some() {
482        machine.add_callable(GpioMockInjectCommand::new(pins.clone()));
483        machine.add_callable(GpioMockTraceFunction::new(pins.clone()));
484    }
485
486    machine.add_clearable(PinsClearable::new(pins.clone()));
487    machine.add_callable(GpioClearCommand::new(pins.clone()));
488    machine.add_callable(GpioReadFunction::new(pins.clone()));
489    machine.add_callable(GpioSetupCommand::new(pins.clone()));
490    machine.add_callable(GpioWriteCommand::new(pins));
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::testutils::*;
497    use futures_lite::future::block_on;
498
499    /// Common checks for pin number validation.
500    ///
501    /// The given input `fmt` string contains the command to test with a placeholder `_PIN` for
502    /// where the pin number goes.  The `short_prefix` and `long_prefix` contain possible prefixes
503    /// for the error messages.
504    fn check_pin_validation(short_prefix: &str, long_prefix: &str, fmt: &str) {
505        check_stmt_compilation_err(
506            format!(r#"{}BOOLEAN is not a number"#, short_prefix),
507            &fmt.replace("_PIN_", "TRUE"),
508        );
509        check_stmt_err(
510            format!(r#"{}Pin number 123456789 is too large"#, long_prefix),
511            &fmt.replace("_PIN_", "123456789"),
512        );
513        check_stmt_err(
514            format!(r#"{}Pin number -1 must be positive"#, long_prefix),
515            &fmt.replace("_PIN_", "-1"),
516        );
517    }
518
519    /// Creates a machine backed by `MockPins` pre-seeded with `reads` and returns both the machine
520    /// and a handle to inspect the trace afterwards.
521    fn make_mock_machine(reads: &[(u8, bool)]) -> (Machine, Rc<RefCell<MockPins>>) {
522        let mock_pins = Rc::new(RefCell::new(MockPins::default()));
523        for &(pin, high) in reads {
524            mock_pins.borrow_mut().inject_read(Pin(pin), high);
525        }
526        let pins: Rc<RefCell<dyn Pins>> = mock_pins.clone();
527        let machine = crate::MachineBuilder::default().with_gpio_pins(pins).build().unwrap();
528        (machine, mock_pins)
529    }
530
531    /// Runs `code` in a machine backed by MockPins pre-seeded with `reads` and asserts that the
532    /// resulting trace equals `expected_trace`.
533    fn do_mock_test(code: &str, reads: &[(u8, bool)], expected_trace: &[i32]) {
534        let (mut machine, mock_pins) = make_mock_machine(reads);
535        let _ = block_on(machine.exec(&mut code.as_bytes())).unwrap();
536        assert_eq!(expected_trace, mock_pins.borrow().trace());
537    }
538
539    /// Tests that all GPIO operations delegate to the real pins implementation, which defaults to
540    /// the no-op backend when using the tester.  All other tests in this file use MockPins via
541    /// `make_mock_machine` to validate operation.
542    #[test]
543    fn test_real_backend() {
544        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_SETUP 0, \"IN\"");
545        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR");
546        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR 0");
547        check_expr_error("1:10: GPIO backend not compiled in", "GPIO_READ(0)");
548        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_WRITE 0, TRUE");
549    }
550
551    #[test]
552    fn test_gpio_setup_ok() {
553        for mode in &["in", "IN"] {
554            do_mock_test(&format!(r#"GPIO_SETUP 5, "{}""#, mode), &[], &[501]);
555            do_mock_test(&format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[], &[501]);
556        }
557        for mode in &["in-pull-down", "IN-PULL-DOWN"] {
558            do_mock_test(&format!(r#"GPIO_SETUP 6, "{}""#, mode), &[], &[602]);
559            do_mock_test(&format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[], &[602]);
560        }
561        for mode in &["in-pull-up", "IN-PULL-UP"] {
562            do_mock_test(&format!(r#"GPIO_SETUP 7, "{}""#, mode), &[], &[703]);
563            do_mock_test(&format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[], &[703]);
564        }
565        for mode in &["out", "OUT"] {
566            do_mock_test(&format!(r#"GPIO_SETUP 8, "{}""#, mode), &[], &[804]);
567            do_mock_test(&format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[], &[804]);
568        }
569    }
570
571    #[test]
572    fn test_gpio_setup_multiple() {
573        do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[], &[1803, 1004]);
574    }
575
576    #[test]
577    fn test_gpio_setup_errors() {
578        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP"#);
579        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1"#);
580        check_stmt_compilation_err("1:15: Expected STRING but found INTEGER", r#"GPIO_SETUP 1; 2"#);
581        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#);
582
583        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_SETUP _PIN_, "IN""#);
584
585        check_stmt_err(r#"1:15: Unknown pin mode IN-OUT"#, r#"GPIO_SETUP 1, "IN-OUT""#);
586    }
587
588    #[test]
589    fn test_gpio_clear_all() {
590        do_mock_test("GPIO_CLEAR", &[], &[-1]);
591    }
592
593    #[test]
594    fn test_gpio_clear_one() {
595        do_mock_test("GPIO_CLEAR 4", &[], &[405]);
596        do_mock_test("GPIO_CLEAR 4.1", &[], &[405]);
597    }
598
599    #[test]
600    fn test_gpio_clear_errors() {
601        check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1,"#);
602        check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1, 2"#);
603
604        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_CLEAR _PIN_"#);
605    }
606
607    #[test]
608    fn test_gpio_read_ok() {
609        // Read pin 3 (low → GPIO_WRITE 5 low), then read pin 3 (high → GPIO_WRITE 7 high).
610        // GPIO_READ evaluates before GPIO_WRITE, so trace is: read3low, write5low, read3high, write7high.
611        do_mock_test(
612            "GPIO_WRITE 5, GPIO_READ(3.1): GPIO_WRITE 7, GPIO_READ(3)",
613            &[(3, false), (3, true)],
614            &[310, 520, 311, 721],
615        );
616    }
617
618    #[test]
619    fn test_gpio_read_errors() {
620        check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#);
621        check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#);
622
623        check_pin_validation("1:15: ", "1:15: ", r#"v = GPIO_READ(_PIN_)"#);
624    }
625
626    #[test]
627    fn test_gpio_write_ok() {
628        do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[], &[321, 320]);
629    }
630
631    #[test]
632    fn test_gpio_write_errors() {
633        check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE"#);
634        check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 2,"#);
635        check_stmt_compilation_err(
636            "1:1: GPIO_WRITE expected pin%, value?",
637            r#"GPIO_WRITE 1, TRUE, 2"#,
638        );
639        check_stmt_compilation_err(
640            "1:13: GPIO_WRITE expected pin%, value?",
641            r#"GPIO_WRITE 1; TRUE"#,
642        );
643
644        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#);
645
646        check_stmt_compilation_err(
647            "1:15: Expected BOOLEAN but found INTEGER",
648            r#"GPIO_WRITE 1, 5"#,
649        );
650    }
651}