1use 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
33const 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
41pub struct Pin(pub u8);
42
43impl Pin {
44 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum PinMode {
59 In,
61
62 InPullDown,
64
65 InPullUp,
67
68 Out,
70}
71
72impl PinMode {
73 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
85pub trait Pins {
87 fn as_any(&self) -> &dyn Any;
89
90 fn as_any_mut(&mut self) -> &mut dyn Any;
92
93 fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
99
100 fn clear(&mut self, pin: Pin) -> io::Result<()>;
102
103 fn clear_all(&mut self) -> io::Result<()>;
105
106 fn read(&mut self, pin: Pin) -> io::Result<bool>;
108
109 fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
111}
112
113pub(crate) struct PinsClearable {
115 pins: Rc<RefCell<dyn Pins>>,
116}
117
118impl PinsClearable {
119 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
131pub struct GpioSetupCommand {
133 metadata: CallableMetadata,
134 pins: Rc<RefCell<dyn Pins>>,
135}
136
137impl GpioSetupCommand {
138 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
200pub struct GpioClearCommand {
202 metadata: CallableMetadata,
203 pins: Rc<RefCell<dyn Pins>>,
204}
205
206impl GpioClearCommand {
207 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
260pub struct GpioReadFunction {
262 metadata: CallableMetadata,
263 pins: Rc<RefCell<dyn Pins>>,
264}
265
266impl GpioReadFunction {
267 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
311pub struct GpioWriteCommand {
313 metadata: CallableMetadata,
314 pins: Rc<RefCell<dyn Pins>>,
315}
316
317impl GpioWriteCommand {
318 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
371pub struct GpioMockInjectCommand {
373 metadata: CallableMetadata,
374 pins: Rc<RefCell<dyn Pins>>,
375}
376
377impl GpioMockInjectCommand {
378 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
437pub struct GpioMockTraceFunction {
439 metadata: CallableMetadata,
440 pins: Rc<RefCell<dyn Pins>>,
441}
442
443impl GpioMockTraceFunction {
444 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
479pub 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 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 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 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 #[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 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}