1use 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
35const 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
43pub struct Pin(pub u8);
44
45impl Pin {
46 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum PinMode {
64 In,
66
67 InPullDown,
69
70 InPullUp,
72
73 Out,
75}
76
77impl PinMode {
78 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
90pub trait Pins {
92 fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
98
99 fn clear(&mut self, pin: Pin) -> io::Result<()>;
101
102 fn clear_all(&mut self) -> io::Result<()>;
104
105 fn read(&mut self, pin: Pin) -> io::Result<bool>;
107
108 fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
110}
111
112pub(crate) struct PinsClearable {
114 pins: Rc<RefCell<dyn Pins>>,
115}
116
117impl PinsClearable {
118 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
133pub struct GpioSetupCommand {
135 metadata: CallableMetadata,
136 pins: Rc<RefCell<dyn Pins>>,
137}
138
139impl GpioSetupCommand {
140 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
205pub struct GpioClearCommand {
207 metadata: CallableMetadata,
208 pins: Rc<RefCell<dyn Pins>>,
209}
210
211impl GpioClearCommand {
212 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
271pub struct GpioReadFunction {
273 metadata: CallableMetadata,
274 pins: Rc<RefCell<dyn Pins>>,
275}
276
277impl GpioReadFunction {
278 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
325pub struct GpioWriteCommand {
327 metadata: CallableMetadata,
328 pins: Rc<RefCell<dyn Pins>>,
329}
330
331impl GpioWriteCommand {
332 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
388pub 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 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 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 fn do_mock_test<S: Into<String>>(code: S, trace: &[i32]) {
458 do_mock_test_with_vars(code, trace, [])
459 }
460
461 #[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}