Skip to content

Commit

Permalink
Initial GeneralState concept
Browse files Browse the repository at this point in the history
  • Loading branch information
scottbez1 committed Jun 11, 2024
1 parent 1679c78 commit a63568d
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 72 deletions.
47 changes: 44 additions & 3 deletions arduino/splitflap/Splitflap/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

// 2) General Settings
#ifndef NUM_MODULES
// Set NUM_MODULES in platformio.ini instead when using ESP32/PlatformIO
#define NUM_MODULES (12)
#endif


// Whether to force a full rotation when the same letter is specified again
#define FORCE_FULL_ROTATION true

Expand All @@ -21,23 +23,62 @@
#define HOME_CALIBRATION_ENABLED true

// 3) Flap Contents & Order
#define NUM_FLAPS (52)
// This `flaps` array should match the order of flaps on your spools, with
// the first being the "home" flap.
//
// These are used for the built-in display, plaintext serial interface, and
// can be referenced by interactive controllers using the proto interface, but
// these are non-critical and only useful for interactive usage with the
// standard interfaces. (If you are using custom control software with the
// proto interface - you'll know if you are - you can ignore these since the
// proto interface uses integer indexes to reference flap positions).
//
// The conventions for these are as follows:
// - basic letters should be UPPER CASE
// - lower-case letters can be used to represent color blocks:
// - g = green
// - p = purple
// - r = red
// - w = white
// - y = yellow

// Flap option 1: Legacy printed flaps (40 per module)
// #define NUM_FLAPS (40)
// const uint8_t flaps[NUM_FLAPS] = {
// ' ',
// 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
// 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
// '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// '.',
// ',',
// '\'',
// };

// This should match the order of flaps on the spool, with the first being the
// "home" flap.
// Flap option 2: v2 flaps (52 per module)
#define NUM_FLAPS (52)
const uint8_t flaps[NUM_FLAPS] = {
' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
'Z', 'g', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'r',
'.', '?', '-', '$', '\'', '#', 'y', 'p', ',', '!', '~', '&', 'w'
};

// Flap option 3: v2 flaps (limited 40-flap set using the first 40 flaps of the set)
// #define NUM_FLAPS (40)
// const uint8_t flaps[NUM_FLAPS] = {
// ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
// 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
// 'Z', 'g', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'r',
// '.',
// };

// Flap option 4: YOUR CUSTOM CHARACTER SET HERE!
// #define NUM_FLAPS (40)
// const uint8_t flaps[NUM_FLAPS] = {
// <FILL THIS IN!>
// };


// 4) Hardware configuration and features
#ifndef SPLITFLAP_PIO_HARDWARE_CONFIG
// Note: these values are only used in the Arduino IDE. For PlatformIO,
Expand Down
6 changes: 6 additions & 0 deletions arduino/splitflap/esp32/proto_gen/splitflap.pb.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ PB_BIND(PB_SupervisorState_PowerChannelState, PB_SupervisorState_PowerChannelSta
PB_BIND(PB_SupervisorState_FaultInfo, PB_SupervisorState_FaultInfo, 2)


PB_BIND(PB_GeneralState, PB_GeneralState, AUTO)


PB_BIND(PB_GeneralState_BuildInfo, PB_GeneralState_BuildInfo, AUTO)


PB_BIND(PB_FromSplitflap, PB_FromSplitflap, 4)


Expand Down
66 changes: 60 additions & 6 deletions arduino/splitflap/esp32/proto_gen/splitflap.pb.h

Large diffs are not rendered by default.

78 changes: 57 additions & 21 deletions arduino/splitflap/esp32/splitflap/serial_proto_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,67 @@ void SerialProtoProtocol::loop() {
packet_serial_.update();
} while (stream_.available());

// Rate limit state change transmissions
bool state_changed = latest_state_ != last_sent_state_ && millis() - last_sent_state_millis_ >= MIN_STATE_INTERVAL_MILLIS;
{
// SplitflapState updates

// Rate limit state change transmissions
bool state_changed = latest_state_ != last_sent_state_ && millis() - last_sent_state_millis_ >= MIN_STATE_INTERVAL_MILLIS;

// Send state periodically or when forced, regardless of rate limit for state changes
bool force_send_state = state_requested_ || millis() - last_sent_state_millis_ > PERIODIC_STATE_INTERVAL_MILLIS;
if (state_changed || force_send_state) {
state_requested_ = false;
pb_tx_buffer_ = {};
pb_tx_buffer_.which_payload = PB_FromSplitflap_splitflap_state_tag;
pb_tx_buffer_.payload.splitflap_state.modules_count = NUM_MODULES;
for (uint8_t i = 0; i < NUM_MODULES; i++) {
pb_tx_buffer_.payload.splitflap_state.modules[i] = {
.state = (PB_SplitflapState_ModuleState_State) latest_state_.modules[i].state,
.flap_index = latest_state_.modules[i].flap_index,
.moving = latest_state_.modules[i].moving,
.home_state = latest_state_.modules[i].home_state,
.count_unexpected_home = latest_state_.modules[i].count_unexpected_home,
.count_missed_home = latest_state_.modules[i].count_missed_home,
};
// Send state periodically or when forced, regardless of rate limit for state changes
bool force_send_state = state_requested_ || millis() - last_sent_state_millis_ > PERIODIC_STATE_INTERVAL_MILLIS;
if (state_changed || force_send_state) {
pb_tx_buffer_ = {};
pb_tx_buffer_.which_payload = PB_FromSplitflap_splitflap_state_tag;
pb_tx_buffer_.payload.splitflap_state.modules_count = NUM_MODULES;
for (uint8_t i = 0; i < NUM_MODULES; i++) {
pb_tx_buffer_.payload.splitflap_state.modules[i] = {
.state = (PB_SplitflapState_ModuleState_State) latest_state_.modules[i].state,
.flap_index = latest_state_.modules[i].flap_index,
.moving = latest_state_.modules[i].moving,
.home_state = latest_state_.modules[i].home_state,
.count_unexpected_home = latest_state_.modules[i].count_unexpected_home,
.count_missed_home = latest_state_.modules[i].count_missed_home,
};
}
#ifdef CHAINLINK
pb_tx_buffer_.payload.splitflap_state.loopbacks_ok = latest_state_.loopbacks_ok;
#endif

sendPbTxBuffer();

last_sent_state_ = latest_state_;
last_sent_state_millis_ = millis();
}
}

{
// GeneralState updates

// Send state periodically or when forced
bool force_send_state = state_requested_ || millis() - last_sent_general_state_millis_ > 2000;
if (force_send_state) {
PB_GeneralState state = {};

sendPbTxBuffer();
state.serial_protocol_version = SERIAL_PROTOCOL_VERSION;
state.uptime_millis = millis();

last_sent_state_ = latest_state_;
last_sent_state_millis_ = millis();
snprintf(state.build_info.git_hash, sizeof(state.build_info.git_hash), BUILD_GIT_HASH);
snprintf(state.build_info.build_date, sizeof(state.build_info.build_date), BUILD_DATE);
snprintf(state.build_info.build_os, sizeof(state.build_info.build_os), BUILD_OS);
state.has_build_info = true;

pb_tx_buffer_ = {};
pb_tx_buffer_.which_payload = PB_FromSplitflap_general_state_tag;
pb_tx_buffer_.payload.general_state = state;
sendPbTxBuffer();

last_sent_general_state_millis_ = millis();
}
}

if (state_requested_) {
// Handled above
state_requested_ = false;
}
}

Expand Down
15 changes: 14 additions & 1 deletion arduino/splitflap/esp32/splitflap/serial_proto_protocol.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2021 Scott Bezek and the splitflap contributors
Copyright 2024 Scott Bezek and the splitflap contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,17 @@
#include "serial_protocol.h"
#include "../proto_gen/splitflap.pb.h"

/*
* Serial protocol version changelog
*
* 0 (unset):
* - No GeneralState message
* - May or may not have software offset support
* 1:
* - GeneralState is introduced (including introduction of serial protocol versioning)
*/
#define SERIAL_PROTOCOL_VERSION (1);

class SerialProtoProtocol : public SerialProtocol {
public:
SerialProtoProtocol(SplitflapTask& splitflap_task, Stream& stream);
Expand All @@ -46,6 +57,8 @@ class SerialProtoProtocol : public SerialProtocol {
SplitflapState last_sent_state_ = {};
uint32_t last_sent_state_millis_ = 0;

uint32_t last_sent_general_state_millis_ = 0;

bool state_requested_;

void sendPbTxBuffer();
Expand Down
2 changes: 1 addition & 1 deletion arduino/splitflap/esp32/splitflap/serial_task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@ void SerialTask::log(const char* msg) {
void SerialTask::sendSupervisorState(PB_SupervisorState& supervisor_state) {
// Only queue the latest supervisor state
xQueueOverwrite(supervisor_state_queue_, &supervisor_state);
}
}
23 changes: 23 additions & 0 deletions proto/splitflap.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ message SplitflapState {
}

repeated ModuleState modules = 1 [(nanopb).max_count = 255];
bool loopbacks_ok = 2;
}

message Log {
Expand All @@ -34,6 +35,7 @@ message Ack {
uint32 nonce = 1;
}

/** Chainlink Base state -- only reported by Chainlink Base firmware, NOT standard Chainlink firmware */
message SupervisorState {
enum State {
UNKNOWN = 0;
Expand Down Expand Up @@ -70,12 +72,32 @@ message SupervisorState {
FaultInfo fault_info = 4;
}


/** Chainlink general state, reported infrequently -- only reported by standard Chainlink firmware, NOT Chainlink Base firmware */
message GeneralState {
uint32 serial_protocol_version = 1 [(nanopb).int_size = IS_16];
uint32 uptime_millis = 2;

message BuildInfo {
string git_hash = 1 [(nanopb).max_length = 90];
string build_date = 2 [(nanopb).max_length = 12];
string build_os = 3 [(nanopb).max_length = 12];
}

BuildInfo build_info = 3;

// TODO: Flap character set
// TODO: Flap layout? (share with display code?)
// TODO: Wifi status?
}

message FromSplitflap {
oneof payload {
SplitflapState splitflap_state = 1;
Log log = 2;
Ack ack = 3;
SupervisorState supervisor_state = 4;
GeneralState general_state = 5;
}
}

Expand Down Expand Up @@ -133,6 +155,7 @@ message ToSplitflap {
}
}

/** Non-volatile on-device storage schema */
message PersistentConfiguration {
uint32 version = 1;
uint32 num_flaps = 2;
Expand Down
31 changes: 29 additions & 2 deletions software/chainlink/js/packages/example-webserial-basic/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {SyntheticEvent, useCallback, useEffect, useState} from 'react'
import React, {SyntheticEvent, useCallback, useEffect, useRef, useState} from 'react'
import Typography from '@mui/material/Typography'
import Container from '@mui/material/Container'
import {PB} from 'splitflapjs-proto'
Expand Down Expand Up @@ -44,6 +44,7 @@ export const App: React.FC<AppProps> = () => {
defaults: true,
}) as NoUndefinedField<PB.ISplitflapState>,
)
const [splitflapGeneralState, setSplitflapGeneralState] = useState<NoUndefinedField<PB.IGeneralState> | null>();
const [inputValue, setInputValue] = useState({val: '', user: true});

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -76,6 +77,8 @@ export const App: React.FC<AppProps> = () => {
const [unsavedCalibration, setUnsavedCalibration] = useState<boolean>(false)
const [logDisplay, setLogDisplay] = useState<LogDisplay | null>(null);

const initializationTimeoutRef = useRef<ReturnType<typeof setTimeout>>()

const connectToSerial = async () => {
try {
if (navigator.serial) {
Expand Down Expand Up @@ -103,11 +106,34 @@ export const App: React.FC<AppProps> = () => {
return newLogs
})
}
} else if (message.payload === 'ack') {
// Ignore (internal protocol implementation detail)
} else if (message.payload === 'generalState' && message.generalState !== null) {
const state = PB.GeneralState.create(message.generalState)
const stateObj = PB.GeneralState.toObject(state, {
defaults: true,
}) as NoUndefinedField<PB.IGeneralState>
setSplitflapGeneralState(stateObj)

const initializationTimeout = initializationTimeoutRef.current;
if (initializationTimeout !== undefined) {
clearTimeout(initializationTimeout)
initializationTimeoutRef.current = undefined;
setSplitflap(splitflap)
}
} else {
console.log('Unhandled message type', message);
}
})
setSplitflap(splitflap)
const loop = splitflap.openAndLoop()
splitflap.sendConfig(PB.SplitflapConfig.create(splitflapConfig))

// Older firmware did not send general state; use a timeout to determine if we should fall back to legacy mode
initializationTimeoutRef.current = setTimeout(() => {
initializationTimeoutRef.current = undefined
console.log('Timed out waiting for initial general state; assuming this is a legacy splitflap connected')
setSplitflap(splitflap)
}, 3000)
await loop
} else {
console.error('Web Serial API is not supported in this browser.')
Expand Down Expand Up @@ -261,6 +287,7 @@ export const App: React.FC<AppProps> = () => {
<Link onClick={() => {
setLogDisplay({lastN: 20, title: "Recent logs", body:""})
}}>View logs</Link>
<pre>{ JSON.stringify(splitflapGeneralState) }</pre>
{ logDisplay !== null ? (
<Dialog open={true} onClose={() => setLogDisplay(null)}>
<DialogTitle>{logDisplay.title}</DialogTitle>
Expand Down
Loading

0 comments on commit a63568d

Please sign in to comment.