1use endbasic_core::{
20 ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
21 CallableMetadataBuilder, ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
22};
23use std::any::Any;
24use std::borrow::Cow;
25use std::cell::RefCell;
26use std::io;
27use std::rc::Rc;
28
29mod fakes;
30pub use fakes::{MockPins, NoopPins};
31
32use crate::{Clearable, MachineBuilder};
33
34const CATEGORY: &str = "Hardware interface
36EndBASIC provides features to manipulate external hardware. These features are currently limited \
37to GPIO interaction on a Raspberry Pi and are only available when EndBASIC has explicitly been \
38built with the --features=rpi option. Support for other busses and platforms may come later.";
39
40#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub struct Pin(pub u8);
43
44impl Pin {
45 fn from_i32(i: i32) -> Result<Self, String> {
47 if i < 0 {
48 return Err(format!("Pin number {} must be positive", i));
49 }
50 if i > u8::MAX as i32 {
51 return Err(format!("Pin number {} is too large", i));
52 }
53 Ok(Self(i as u8))
54 }
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum PinMode {
60 In,
62
63 InPullDown,
65
66 InPullUp,
68
69 Out,
71}
72
73impl PinMode {
74 fn parse(s: &str) -> Result<PinMode, String> {
76 match s.to_ascii_uppercase().as_ref() {
77 "IN" => Ok(PinMode::In),
78 "IN-PULL-UP" => Ok(PinMode::InPullUp),
79 "IN-PULL-DOWN" => Ok(PinMode::InPullDown),
80 "OUT" => Ok(PinMode::Out),
81 s => Err(format!("Unknown pin mode {}", s)),
82 }
83 }
84}
85
86fn parse_pin(scope: &Scope<'_>, narg: u8) -> CallResult<Pin> {
87 Pin::from_i32(scope.get_integer(narg)).map_err(|e| CallError::Syntax(scope.get_pos(narg), e))
88}
89
90fn parse_pin_mode(scope: &Scope<'_>, narg: u8) -> CallResult<PinMode> {
91 PinMode::parse(scope.get_string(narg)).map_err(|e| CallError::Syntax(scope.get_pos(narg), e))
92}
93
94pub trait Pins {
96 fn as_any(&self) -> &dyn Any;
98
99 fn as_any_mut(&mut self) -> &mut dyn Any;
101
102 fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
108
109 fn clear(&mut self, pin: Pin) -> io::Result<()>;
111
112 fn clear_all(&mut self) -> io::Result<()>;
114
115 fn read(&mut self, pin: Pin) -> io::Result<bool>;
117
118 fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
120}
121
122pub(crate) struct PinsClearable {
124 pins: Rc<RefCell<dyn Pins>>,
125}
126
127impl PinsClearable {
128 pub(crate) fn new(pins: Rc<RefCell<dyn Pins>>) -> Box<Self> {
130 Box::from(Self { pins })
131 }
132}
133
134impl Clearable for PinsClearable {
135 fn reset_state(&self) {
136 let _ = self.pins.borrow_mut().clear_all();
137 }
138}
139
140pub struct GpioSetupCommand {
142 metadata: Rc<CallableMetadata>,
143 pins: Rc<RefCell<dyn Pins>>,
144}
145
146impl GpioSetupCommand {
147 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
149 Rc::from(Self {
150 metadata: CallableMetadataBuilder::new("GPIO_SETUP")
151 .with_syntax(&[(
152 &[
153 SingularArgSyntax::RequiredValue(
154 RequiredValueSyntax {
155 name: Cow::Borrowed("pin"),
156 vtype: ExprType::Integer,
157 },
158 ArgSepSyntax::Exactly(ArgSep::Long),
159 ),
160 SingularArgSyntax::RequiredValue(
161 RequiredValueSyntax {
162 name: Cow::Borrowed("mode"),
163 vtype: ExprType::Text,
164 },
165 ArgSepSyntax::End,
166 ),
167 ],
168 None,
169 )])
170 .with_category(CATEGORY)
171 .with_description(
172 "Configures a GPIO pin for input or output.
173Before a GPIO pin can be used for reads or writes, it must be configured to be an input or \
174output pin. Additionally, if pull up or pull down resistors are available and desired, these \
175must be configured upfront too.
176The mode$ has to be one of \"IN\", \"IN-PULL-DOWN\", \"IN-PULL-UP\", or \"OUT\". These values \
177are case-insensitive. The possibility of using the pull-down and pull-up resistors depends on \
178whether they are available in the hardware, and selecting these modes will fail if they are not.
179It is OK to reconfigure an already configured pin without clearing its state first.",
180 )
181 .build(),
182 pins,
183 })
184 }
185}
186
187impl Callable for GpioSetupCommand {
188 fn metadata(&self) -> Rc<CallableMetadata> {
189 self.metadata.clone()
190 }
191
192 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
193 debug_assert_eq!(2, scope.nargs());
194 let pin = parse_pin(&scope, 0)?;
195 let mode = parse_pin_mode(&scope, 1)?;
196
197 self.pins.borrow_mut().setup(pin, mode).map_err(CallError::from)?;
198 Ok(())
199 }
200}
201
202pub struct GpioClearCommand {
204 metadata: Rc<CallableMetadata>,
205 pins: Rc<RefCell<dyn Pins>>,
206}
207
208impl GpioClearCommand {
209 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
211 Rc::from(Self {
212 metadata: CallableMetadataBuilder::new("GPIO_CLEAR")
213 .with_syntax(&[
214 (&[], None),
215 (
216 &[SingularArgSyntax::RequiredValue(
217 RequiredValueSyntax {
218 name: Cow::Borrowed("pin"),
219 vtype: ExprType::Integer,
220 },
221 ArgSepSyntax::End,
222 )],
223 None,
224 ),
225 ])
226 .with_category(CATEGORY)
227 .with_description(
228 "Resets the GPIO chip or a specific pin.
229If no pin% is specified, resets the state of all GPIO pins. \
230If a pin% is given, only that pin is reset. It is OK if the given pin has never been configured \
231before.",
232 )
233 .build(),
234 pins,
235 })
236 }
237}
238
239impl Callable for GpioClearCommand {
240 fn metadata(&self) -> Rc<CallableMetadata> {
241 self.metadata.clone()
242 }
243
244 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
245 if scope.nargs() == 0 {
246 self.pins.borrow_mut().clear_all().map_err(CallError::from)?;
247 } else {
248 debug_assert_eq!(1, scope.nargs());
249 let pin = parse_pin(&scope, 0)?;
250
251 self.pins.borrow_mut().clear(pin).map_err(CallError::from)?;
252 }
253
254 Ok(())
255 }
256}
257
258pub struct GpioReadFunction {
260 metadata: Rc<CallableMetadata>,
261 pins: Rc<RefCell<dyn Pins>>,
262}
263
264impl GpioReadFunction {
265 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
267 Rc::from(Self {
268 metadata: CallableMetadataBuilder::new("GPIO_READ")
269 .with_return_type(ExprType::Boolean)
270 .with_syntax(&[(
271 &[SingularArgSyntax::RequiredValue(
272 RequiredValueSyntax {
273 name: Cow::Borrowed("pin"),
274 vtype: ExprType::Integer,
275 },
276 ArgSepSyntax::End,
277 )],
278 None,
279 )])
280 .with_category(CATEGORY)
281 .with_description(
282 "Reads the state of a GPIO pin.
283Returns FALSE to represent a low value, and TRUE to represent a high value.",
284 )
285 .build(),
286 pins,
287 })
288 }
289}
290
291impl Callable for GpioReadFunction {
292 fn metadata(&self) -> Rc<CallableMetadata> {
293 self.metadata.clone()
294 }
295
296 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
297 debug_assert_eq!(1, scope.nargs());
298 let pin = parse_pin(&scope, 0)?;
299
300 let value = self.pins.borrow_mut().read(pin).map_err(CallError::from)?;
301 scope.return_boolean(value)
302 }
303}
304
305pub struct GpioWriteCommand {
307 metadata: Rc<CallableMetadata>,
308 pins: Rc<RefCell<dyn Pins>>,
309}
310
311impl GpioWriteCommand {
312 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
314 Rc::from(Self {
315 metadata: CallableMetadataBuilder::new("GPIO_WRITE")
316 .with_syntax(&[(
317 &[
318 SingularArgSyntax::RequiredValue(
319 RequiredValueSyntax {
320 name: Cow::Borrowed("pin"),
321 vtype: ExprType::Integer,
322 },
323 ArgSepSyntax::Exactly(ArgSep::Long),
324 ),
325 SingularArgSyntax::RequiredValue(
326 RequiredValueSyntax {
327 name: Cow::Borrowed("value"),
328 vtype: ExprType::Boolean,
329 },
330 ArgSepSyntax::End,
331 ),
332 ],
333 None,
334 )])
335 .with_category(CATEGORY)
336 .with_description(
337 "Sets the state of a GPIO pin.
338A FALSE value? sets the pin to low, and a TRUE value? sets the pin to high.",
339 )
340 .build(),
341 pins,
342 })
343 }
344}
345
346impl Callable for GpioWriteCommand {
347 fn metadata(&self) -> Rc<CallableMetadata> {
348 self.metadata.clone()
349 }
350
351 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
352 debug_assert_eq!(2, scope.nargs());
353 let pin = parse_pin(&scope, 0)?;
354 let value = scope.get_boolean(1);
355
356 self.pins.borrow_mut().write(pin, value).map_err(CallError::from)?;
357 Ok(())
358 }
359}
360
361pub struct GpioMockInjectCommand {
363 metadata: Rc<CallableMetadata>,
364 pins: Rc<RefCell<dyn Pins>>,
365}
366
367impl GpioMockInjectCommand {
368 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
370 Rc::from(Self {
371 metadata: CallableMetadataBuilder::new("GPIO_MOCK_INJECT")
372 .with_syntax(&[(
373 &[
374 SingularArgSyntax::RequiredValue(
375 RequiredValueSyntax {
376 name: Cow::Borrowed("pin"),
377 vtype: ExprType::Integer,
378 },
379 ArgSepSyntax::Exactly(ArgSep::Long),
380 ),
381 SingularArgSyntax::RequiredValue(
382 RequiredValueSyntax {
383 name: Cow::Borrowed("high"),
384 vtype: ExprType::Boolean,
385 },
386 ArgSepSyntax::End,
387 ),
388 ],
389 None,
390 )])
391 .with_category(CATEGORY)
392 .with_description(
393 "Pre-seeds a GPIO_READ result for testing.
394This command is only available when EndBASIC is started with --gpio-pins=mock. It pre-seeds \
395the next GPIO_READ call for the given pin% to return the given high? value.",
396 )
397 .build(),
398 pins,
399 })
400 }
401}
402
403impl Callable for GpioMockInjectCommand {
404 fn metadata(&self) -> Rc<CallableMetadata> {
405 self.metadata.clone()
406 }
407
408 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
409 debug_assert_eq!(2, scope.nargs());
410 let pin = parse_pin(&scope, 0)?;
411 let high = scope.get_boolean(1);
412
413 self.pins
414 .borrow_mut()
415 .as_any_mut()
416 .downcast_mut::<MockPins>()
417 .expect("Only registered for mock backend")
418 .inject_read(pin, high);
419 Ok(())
420 }
421}
422
423pub struct GpioMockTraceFunction {
425 metadata: Rc<CallableMetadata>,
426 pins: Rc<RefCell<dyn Pins>>,
427}
428
429impl GpioMockTraceFunction {
430 pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
432 Rc::from(Self {
433 metadata: CallableMetadataBuilder::new("GPIO_MOCK_TRACE")
434 .with_return_type(ExprType::Text)
435 .with_syntax(&[(&[], None)])
436 .with_category(CATEGORY)
437 .with_description(
438 "Returns the GPIO operation trace for testing.
439This function is only available when EndBASIC is started with --gpio-pins=mock. It returns a \
440space-separated list of integers representing the ordered record of all GPIO operations \
441performed since the last reset.",
442 )
443 .build(),
444 pins,
445 })
446 }
447}
448
449impl Callable for GpioMockTraceFunction {
450 fn metadata(&self) -> Rc<CallableMetadata> {
451 self.metadata.clone()
452 }
453
454 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
455 debug_assert_eq!(0, scope.nargs());
456 let pins = self.pins.borrow();
457 let mock =
458 pins.as_any().downcast_ref::<MockPins>().expect("Only registered for mock backend");
459 let result = mock.trace().iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
460 scope.return_string(result)
461 }
462}
463
464pub fn add_all(machine: &mut MachineBuilder, pins: Rc<RefCell<dyn Pins>>) {
466 if pins.borrow().as_any().downcast_ref::<MockPins>().is_some() {
467 machine.add_callable(GpioMockInjectCommand::new(pins.clone()));
468 machine.add_callable(GpioMockTraceFunction::new(pins.clone()));
469 }
470
471 machine.add_clearable(PinsClearable::new(pins.clone()));
472 machine.add_callable(GpioClearCommand::new(pins.clone()));
473 machine.add_callable(GpioReadFunction::new(pins.clone()));
474 machine.add_callable(GpioSetupCommand::new(pins.clone()));
475 machine.add_callable(GpioWriteCommand::new(pins));
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::testutils::*;
482 use futures_lite::future::block_on;
483
484 fn check_pin_validation(short_prefix: &str, long_prefix: &str, fmt: &str) {
490 check_stmt_compilation_err(
491 format!(r#"{}BOOLEAN is not a number"#, short_prefix),
492 &fmt.replace("_PIN_", "TRUE"),
493 );
494 check_stmt_err(
495 format!(r#"{}Pin number 123456789 is too large"#, long_prefix),
496 &fmt.replace("_PIN_", "123456789"),
497 );
498 check_stmt_err(
499 format!(r#"{}Pin number -1 must be positive"#, long_prefix),
500 &fmt.replace("_PIN_", "-1"),
501 );
502 }
503
504 fn make_mock_machine(reads: &[(u8, bool)]) -> (crate::Machine, Rc<RefCell<MockPins>>) {
507 let mock_pins = Rc::new(RefCell::new(MockPins::default()));
508 for &(pin, high) in reads {
509 mock_pins.borrow_mut().inject_read(Pin(pin), high);
510 }
511 let pins: Rc<RefCell<dyn Pins>> = mock_pins.clone();
512 let machine = MachineBuilder::default().with_gpio_pins(pins).build();
513 (machine, mock_pins)
514 }
515
516 fn do_mock_test(code: &str, reads: &[(u8, bool)], expected_trace: &[i32]) {
519 let (mut machine, mock_pins) = make_mock_machine(reads);
520 machine.compile(&mut code.as_bytes()).unwrap();
521 let _ = block_on(machine.exec()).unwrap();
522 assert_eq!(expected_trace, mock_pins.borrow().trace());
523 }
524
525 #[test]
529 fn test_real_backend() {
530 check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_SETUP 0, \"IN\"");
531 check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR");
532 check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR 0");
533 check_expr_error("1:10: GPIO backend not compiled in", "GPIO_READ(0)");
534 check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_WRITE 0, TRUE");
535 }
536
537 #[test]
538 fn test_gpio_setup_ok() {
539 for mode in &["in", "IN"] {
540 do_mock_test(&format!(r#"GPIO_SETUP 5, "{}""#, mode), &[], &[501]);
541 do_mock_test(&format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[], &[501]);
542 }
543 for mode in &["in-pull-down", "IN-PULL-DOWN"] {
544 do_mock_test(&format!(r#"GPIO_SETUP 6, "{}""#, mode), &[], &[602]);
545 do_mock_test(&format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[], &[602]);
546 }
547 for mode in &["in-pull-up", "IN-PULL-UP"] {
548 do_mock_test(&format!(r#"GPIO_SETUP 7, "{}""#, mode), &[], &[703]);
549 do_mock_test(&format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[], &[703]);
550 }
551 for mode in &["out", "OUT"] {
552 do_mock_test(&format!(r#"GPIO_SETUP 8, "{}""#, mode), &[], &[804]);
553 do_mock_test(&format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[], &[804]);
554 }
555 }
556
557 #[test]
558 fn test_gpio_setup_multiple() {
559 do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[], &[1803, 1004]);
560 }
561
562 #[test]
563 fn test_gpio_setup_errors() {
564 check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP"#);
565 check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1"#);
566 check_stmt_compilation_err("1:13: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1; 2"#);
567 check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#);
568
569 check_pin_validation("1:12: ", "1:12: ", r#"GPIO_SETUP _PIN_, "IN""#);
570
571 check_stmt_err(r#"1:15: Unknown pin mode IN-OUT"#, r#"GPIO_SETUP 1, "IN-OUT""#);
572 }
573
574 #[test]
575 fn test_gpio_clear_all() {
576 do_mock_test("GPIO_CLEAR", &[], &[-1]);
577 }
578
579 #[test]
580 fn test_gpio_clear_one() {
581 do_mock_test("GPIO_CLEAR 4", &[], &[405]);
582 do_mock_test("GPIO_CLEAR 4.1", &[], &[405]);
583 }
584
585 #[test]
586 fn test_gpio_clear_errors() {
587 check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1,"#);
588 check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1, 2"#);
589
590 check_pin_validation("1:12: ", "1:12: ", r#"GPIO_CLEAR _PIN_"#);
591 }
592
593 #[test]
594 fn test_gpio_read_ok() {
595 do_mock_test(
598 "GPIO_WRITE 5, GPIO_READ(3.1): GPIO_WRITE 7, GPIO_READ(3)",
599 &[(3, false), (3, true)],
600 &[310, 520, 311, 721],
601 );
602 }
603
604 #[test]
605 fn test_gpio_read_errors() {
606 check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#);
607 check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#);
608
609 check_pin_validation("1:15: ", "1:15: ", r#"v = GPIO_READ(_PIN_)"#);
610 }
611
612 #[test]
613 fn test_gpio_write_ok() {
614 do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[], &[321, 320]);
615 }
616
617 #[test]
618 fn test_gpio_write_errors() {
619 check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE"#);
620 check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 2,"#);
621 check_stmt_compilation_err(
622 "1:1: GPIO_WRITE expected pin%, value?",
623 r#"GPIO_WRITE 1, TRUE, 2"#,
624 );
625 check_stmt_compilation_err(
626 "1:13: GPIO_WRITE expected pin%, value?",
627 r#"GPIO_WRITE 1; TRUE"#,
628 );
629
630 check_pin_validation("1:12: ", "1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#);
631
632 check_stmt_compilation_err(
633 "1:15: Expected BOOLEAN but found INTEGER",
634 r#"GPIO_WRITE 1, 5"#,
635 );
636 }
637}