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